Chapter 6. Converting playbooks for AAP2 With Ansible Automation Platform 2 and its containerised execution environments, the usage of localhost has been altered. In versions of Ansible Automation Platform, a job would run against localhost , which translated into running on the underlying Automation Controller host. This could be used to store data and persistent artifacts. With Ansible Automation Platform 2, localhost means you are running inside a container, which is ephemeral in nature. Localhost is no longer tied to a particular host, and with portable execution environments, this means it can run anywhere with the right environment and software prerequisites already embedded into the execution environment container. 6.1. Persisting data from auto runs Consider the local automation controller filesystem as counterproductive since that ties the data to that host. If you have a multi-node cluster, then you can contact a different host each time, causing issues if you are creating workflows that depend on each other and created directories. For example, if a directory was only created in one node while another node runs the playbook, the results would be inconsistent. The solution is to use some form of shared storage solution, such as Amazon S3, Gist, or a role to rsync data to your data endpoint. The option exists of injecting data or a configuration into a container at runtime. This can be achieved by using the automation controller's isolated jobs path option. This provides a way to mount directories and files into an execution environment at runtime. This is achieved through the automation mesh, using ansible-runner to inject them into a Podman container to start the automation. What follows are some of the use cases for using isolated job paths: Providing SSL certificates at runtime, rather than baking them into an execution environment. Passing runtime configuration data, such as SSH config settings, but could be anything you want to use during automation. Reading and writing to files used before, during and after automation runs. There are caveats to utilization: The volume mount has to pre-exist on all nodes capable of automation execution (so hybrid control plane nodes and all execution nodes). Where SELinux is enabled (Ansible Automation Platform default) beware of file permissions. This is important since rootless Podman is run on non-OCP based installs. The caveats need to be carefully observed. It is highly recommended to read up on rootless Podman and the Podman volume mount runtime options, the [:OPTIONS] part of the isolated job paths, as this is what is used inside Ansible Automation Platform 2. Additional resources Understanding rootless Podman . Podman volume mount runtime options . 6.1.1. Converting playbook examples Examples This example is of a shared directory called /mydata in which we want to be able to read and write files to during a job run. Remember this has to already exist on the execution node we will be using for the automation run. You will target the aape1.local execution node to run this job, because the underlying hosts already has this in place. [awx@aape1 ~]USD ls -la /mydata/ total 4 drwxr-xr-x. 2 awx awx 41 Apr 28 09:27 . dr-xr-xr-x. 19 root root 258 Apr 11 15:16 .. -rw-r--r--. 1 awx awx 33 Apr 11 12:34 file_read -rw-r--r--. 1 awx awx 0 Apr 28 09:27 file_write You will use a simple playbook to launch the automation with sleep defined to allow you access, and to understand the process, as well as demonstrate reading and writing to files. # vim:ft=ansible: - hosts: all gather_facts: false ignore_errors: yes vars: period: 120 myfile: /mydata/file tasks: - name: Collect only selected facts ansible.builtin.setup: filter: - 'ansible_distribution' - 'ansible_machine_id' - 'ansible_memtotal_mb' - 'ansible_memfree_mb' - name: "I'm feeling real sleepy..." ansible.builtin.wait_for: timeout: "{{ period }}" delegate_to: localhost - ansible.builtin.debug: msg: "Isolated paths mounted into execution node: {{ AWX_ISOLATIONS_PATHS }}" - name: "Read pre-existing file..." ansible.builtin.debug: msg: "{{ lookup('file', '{{ myfile }}_read' - name: "Write to a new file..." ansible.builtin.copy: dest: "{{ myfile }}_write" content: | This is the file I've just written to. - name: "Read written out file..." ansible.builtin.debug: msg: "{{ lookup('file', '{{ myfile }}_write') }}" From the Ansible Automation Platform 2 navigation panel, select Settings . Then select Job settings from the Jobs option. Paths to expose isolated jobs: [ "/mydata:/mydata:rw" ] The volume mount is mapped with the same name in the container and has read-write capability. This will get used when you launch the job template. The prompt on launch should be set for extra_vars so you can adjust the sleep duration for each run, The default is 30 seconds. Once launched, and the wait_for module is invoked for the sleep, you can go onto the execution node and look at what is running. To verify the run has completed successfully, run this command to get an output of the job: USD podman exec -it 'podman ps -q' /bin/bash bash-4.4# You are now inside the running execution environment container. Look at the permissions, you will see that awx has become 'root', but this is not really root as in the superuser, as you are using rootless Podman, which maps users into a kernel namespace similar to a sandbox. Learn more about How does rootless Podman work? for shadow-utils. bash-4.4# ls -la /mydata/ Total 4 drwxr-xr-x. 2 root root 41 Apr 28 09:27 . dr-xr-xr-x. 1 root root 77 Apr 28 09:40 .. -rw-r---r-. 1 root root 33 Apr 11 12:34 file_read -rw-r---r-. 1 root root 0 Apr 28 09:27 file_write According to the results, this job failed. In order to understand why, the remaining output needs to be examined. TASK [Read pre-existing file...]******************************* 10:50:12 ok: [localhost] => { "Msg": "This is the file I am reading in." TASK {Write to a new file...}********************************* 10:50:12 An exception occurred during task execution. To see the full traceback, use -vvv. The error was: PermissionError: [Errno 13] Permission denied: b'/mydata/.ansible_tmpazyqyqdrfile_write' -> b' /mydata/file_write' Fatal: [localhost]: FAILED! => {"changed": false, :checksum": "9f576o85d584287a3516ee8b3385cc6f69bf9ce", "msg": "Unable to make b'/root/.ansible/tmp/anisible-tim-1651139412.9808054-40-91081834383738/source' into /mydata/file_write, failed final rename from b'/mydata/.ansible_tmpazyqyqdrfile_write': [Errno 13] Permission denied: b'/mydata/.ansible_tmpazyqyqdrfile_write' -> b'/mydata/file_write} ...ignoring TASK [Read written out file...] ****************************** 10:50:13 Fatal: [localhost]: FAILED: => {"msg": "An unhandled exception occurred while running the lookup plugin 'file'. Error was a <class 'ansible.errors.AnsibleError;>, original message: could not locate file in lookup: /mydate/file_write. Vould not locate file in lookup: /mydate/file_write"} ...ignoring The job failed, even though :rw is set, so it should have write capability. The process was able to read the existing file, but not write out. This is due to SELinux protection that requires proper labels to be placed on the volume content mounted into the container. If the label is missing, SELinux may prevent the process from running inside the container. Labels set by the OS are not changed by Podman. See the Podman documentation for more information. This could be a common misinterpretation. We have set the default to :z , which tells Podman to relabel file objects on shared volumes. So we can either add :z or leave it off. Paths to expose isolated jobs: [ "/mydata:/mydata" ] The playbook will now work as expected: PLAY [all] **************************************************** 11:05:52 TASK [I'm feeling real sleepy. . .] *************************** 11:05:52 ok: [localhost] TASK [Read pre-existing file...] ****************************** 11:05:57 ok: [localhost] => { "Msg": "This is the file I'm reading in." } TASK [Write to a new file...] ********************************** 11:05:57 ok: [localhost] TASK [Read written out file...] ******************************** 11:05:58 ok: [localhost] => { "Msg": "This is the file I've just written to." Back on the underlying execution node host, we have the newly written out contents. Note If you are using container groups to launch automation jobs inside Red Hat OpenShift, you can also tell Ansible Automation Platform 2 to expose the same paths to that environment, but you must toggle the default to On under settings. Once enabled, this will inject this as volumeMounts and volumes inside the pod spec that will be used for execution. It will look like this: apiVersion: v1 kind: Pod Spec: containers: - image: registry.redhat.io/ansible-automation-platform-24/ee-minimal-rhel8 args: - ansible runner - worker - -private-data-dir=/runner volumeMounts: mountPath: /mnt2 name: volume-0 readOnly: true mountPath: /mnt3 name: volume-1 readOnly: true mountPath: /mnt4 name: volume-2 readOnly: true volumes: hostPath: path: /mnt2 type: "" name: volume-0 hostPath: path: /mnt3 type: "" name: volume-1 hostPath: path: /mnt4 type: "" name: volume-2 Storage inside the running container is using the overlay file system. Any modifications inside the running container are destroyed after the job completes, much like a tmpfs being unmounted.

Commands:
[awx@aape1 ~]USD ls -la /mydata/ total 4 drwxr-xr-x. 2 awx awx 41 Apr 28 09:27 . dr-xr-xr-x. 19 root root 258 Apr 11 15:16 .. -rw-r--r--. 1 awx awx 33 Apr 11 12:34 file_read -rw-r--r--. 1 awx awx 0 Apr 28 09:27 file_write
vim:ft=ansible:
- hosts: all gather_facts: false ignore_errors: yes vars: period: 120 myfile: /mydata/file tasks: - name: Collect only selected facts ansible.builtin.setup: filter: - 'ansible_distribution' - 'ansible_machine_id' - 'ansible_memtotal_mb' - 'ansible_memfree_mb' - name: "I'm feeling real sleepy..." ansible.builtin.wait_for: timeout: "{{ period }}" delegate_to: localhost - ansible.builtin.debug: msg: "Isolated paths mounted into execution node: {{ AWX_ISOLATIONS_PATHS }}" - name: "Read pre-existing file..." ansible.builtin.debug: msg: "{{ lookup('file', '{{ myfile }}_read' - name: "Write to a new file..." ansible.builtin.copy: dest: "{{ myfile }}_write" content: | This is the file I've just written to. - name: "Read written out file..." ansible.builtin.debug: msg: "{{ lookup('file', '{{ myfile }}_write') }}"
/mydata:/mydata:rw
podman exec -it 'podman ps -q' /bin/bash bash-4.4#
bash-4.4# ls -la /mydata/ Total 4 drwxr-xr-x. 2 root root 41 Apr 28 09:27 . dr-xr-xr-x. 1 root root 77 Apr 28 09:40 .. -rw-r---r-. 1 root root 33 Apr 11 12:34 file_read -rw-r---r-. 1 root root 0 Apr 28 09:27 file_write
TASK [Read pre-existing file...]******************************* 10:50:12 ok: [localhost] => { "Msg": "This is the file I am reading in." TASK {Write to a new file...}********************************* 10:50:12 An exception occurred during task execution. To see the full traceback, use -vvv. The error was: PermissionError: [Errno 13] Permission denied: b'/mydata/.ansible_tmpazyqyqdrfile_write' -> b' /mydata/file_write' Fatal: [localhost]: FAILED! => {"changed": false, :checksum": "9f576o85d584287a3516ee8b3385cc6f69bf9ce", "msg": "Unable to make b'/root/.ansible/tmp/anisible-tim-1651139412.9808054-40-91081834383738/source' into /mydata/file_write, failed final rename from b'/mydata/.ansible_tmpazyqyqdrfile_write': [Errno 13] Permission denied: b'/mydata/.ansible_tmpazyqyqdrfile_write' -> b'/mydata/file_write} ...ignoring TASK [Read written out file...] ****************************** 10:50:13 Fatal: [localhost]: FAILED: => {"msg": "An unhandled exception occurred while running the lookup plugin 'file'. Error was a <class 'ansible.errors.AnsibleError;>, original message: could not locate file in lookup: /mydate/file_write. Vould not locate file in lookup: /mydate/file_write"} ...ignoring
/mydata:/mydata
PLAY [all] **************************************************** 11:05:52 TASK [I'm feeling real sleepy. . .] *************************** 11:05:52 ok: [localhost] TASK [Read pre-existing file...] ****************************** 11:05:57 ok: [localhost] => { "Msg": "This is the file I'm reading in." } TASK [Write to a new file...] ********************************** 11:05:57 ok: [localhost] TASK [Read written out file...] ******************************** 11:05:58 ok: [localhost] => { "Msg": "This is the file I've just written to."
apiVersion: v1 kind: Pod Spec: containers: - image: registry.redhat.io/ansible-automation-platform-24/ee-minimal-rhel8 args: - ansible runner - worker - -private-data-dir=/runner volumeMounts: mountPath: /mnt2 name: volume-0 readOnly: true mountPath: /mnt3 name: volume-1 readOnly: true mountPath: /mnt4 name: volume-2 readOnly: true volumes: hostPath: path: /mnt2 type: "" name: volume-0 hostPath: path: /mnt3 type: "" name: volume-1 hostPath: path: /mnt4 type: "" name: volume-2