From 8996b83adad98dfff94064405f3674798f7ccc54 Mon Sep 17 00:00:00 2001
From: Rich Megginson <rmeggins@redhat.com>
Date: Wed, 15 Nov 2023 14:28:43 -0700
Subject: [PATCH] feat: support for ostree systems

Feature: Allow running and testing the role with ostree managed nodes.

Reason: We have users who want to use the role to manage ostree
systems.

Result: Users can use the role to manage ostree managed nodes.
Signed-off-by: Rich Megginson <rmeggins@redhat.com>
---
 .ansible-lint                    |   1 +
 .ostree/README.md                |   3 +
 .ostree/get_ostree_data.sh       | 123 +++++++++++++++++++++++++++++++
 .ostree/packages-runtime.txt     |   4 +
 .ostree/packages-testing.txt     |   1 +
 .sanity-ansible-ignore-2.12.txt  |   1 +
 .sanity-ansible-ignore-2.13.txt  |   1 +
 .sanity-ansible-ignore-2.14.txt  |   1 +
 .sanity-ansible-ignore-2.15.txt  |   1 +
 .sanity-ansible-ignore-2.9.txt   |   1 +
 README-ostree.md                 |  66 +++++++++++++++++
 meta/collection-requirements.yml |   2 +
 tasks/set_vars.yml               |  18 +++++
 tests/tasks/setup.yml            |  24 ++++++
 tests/tests_cockpit.yml          |   5 ++
 tests/tests_default.yml          |   4 +-
 tests/tests_default_vars.yml     |   3 +
 tests/tests_example.yml          |   3 +
 tests/tests_sssd.yml             |   3 +
 19 files changed, 264 insertions(+), 1 deletion(-)
 create mode 100644 .ostree/README.md
 create mode 100755 .ostree/get_ostree_data.sh
 create mode 100644 .ostree/packages-runtime.txt
 create mode 100644 .ostree/packages-testing.txt
 create mode 100644 .sanity-ansible-ignore-2.12.txt
 create mode 100644 .sanity-ansible-ignore-2.13.txt
 create mode 100644 .sanity-ansible-ignore-2.14.txt
 create mode 100644 .sanity-ansible-ignore-2.15.txt
 create mode 100644 .sanity-ansible-ignore-2.9.txt
 create mode 100644 README-ostree.md
 create mode 100644 tests/tasks/setup.yml

diff --git a/.ansible-lint b/.ansible-lint
index 1474aad..34c98ee 100644
--- a/.ansible-lint
+++ b/.ansible-lint
@@ -22,5 +22,6 @@ exclude_paths:
   - examples/roles/
 mock_modules:
   - ini_file
+  - ansible.utils.update_fact
 mock_roles:
   - linux-system-roles.tlog
diff --git a/.ostree/README.md b/.ostree/README.md
new file mode 100644
index 0000000..f5e6931
--- /dev/null
+++ b/.ostree/README.md
@@ -0,0 +1,3 @@
+*NOTE*: The `*.txt` files are used by `get_ostree_data.sh` to create the lists
+of packages, and to find other system roles used by this role.  DO NOT use them
+directly.
diff --git a/.ostree/get_ostree_data.sh b/.ostree/get_ostree_data.sh
new file mode 100755
index 0000000..7c32524
--- /dev/null
+++ b/.ostree/get_ostree_data.sh
@@ -0,0 +1,123 @@
+#!/bin/bash
+
+set -euo pipefail
+
+role_collection_dir="${ROLE_COLLECTION_DIR:-fedora/linux_system_roles}"
+ostree_dir="${OSTREE_DIR:-"$(dirname "$(realpath "$0")")"}"
+
+if [ -z "${4:-}" ] || [ "${1:-}" = help ] || [ "${1:-}" = -h ]; then
+    cat <<EOF
+Usage: $0 packages [runtime|testing] DISTRO-MAJOR[.MINOR] [json|yaml|raw|toml]
+The script will use the packages and roles files in $ostree_dir to
+construct the list of packages needed to build the ostree image.  The script
+will output the list of packages in the given format
+- json is a JSON list like ["pkg1","pkg2",....,"pkgN"]
+- yaml is the YAML list format
+- raw is the list of packages, one per line
+- toml is a list of [[packages]] elements as in https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/composing_installing_and_managing_rhel_for_edge_images/index#creating-an-image-builder-blueprint-for-a-rhel-for-edge-image-using-the-command-line-interface_composing-a-rhel-for-edge-image-using-image-builder-command-line
+The DISTRO-MAJOR.MINOR is the same format used by Ansible for distribution e.g. CentOS-8, RedHat-8.9, etc.
+EOF
+    exit 1
+fi
+category="$1"
+pkgtype="$2"
+distro_ver="$3"
+format="$4"
+pkgtypes=("$pkgtype")
+if [ "$pkgtype" = testing ]; then
+    pkgtypes+=(runtime)
+fi
+
+get_rolepath() {
+    local ostree_dir role rolesdir roles_parent_dir
+    ostree_dir="$1"
+    role="$2"
+    roles_parent_dir="$(dirname "$(dirname "$ostree_dir")")"
+    rolesdir="$roles_parent_dir/$role/.ostree"
+    # assumes collection format
+    if [ -d "$rolesdir" ]; then
+        echo "$rolesdir"
+        return 0
+    fi
+    # assumes legacy role format like linux-system-roles.$role/
+    for rolesdir in "$roles_parent_dir"/*-system-roles."$role"/.ostree; do
+        if [ -d "$rolesdir" ]; then
+          echo "$rolesdir"
+          return 0
+        fi
+    done
+    # look elsewhere
+    if [ -n "${ANSIBLE_COLLECTIONS_PATHS:-}" ]; then
+        for pth in ${ANSIBLE_COLLECTIONS_PATHS//:/ }; do
+            rolesdir="$pth/ansible_collections/$role_collection_dir/roles/$role/.ostree"
+            if [ -d "$rolesdir" ]; then
+                echo "$rolesdir"
+                return 0
+            fi
+        done
+    fi
+    return 1
+}
+
+get_packages() {
+    local ostree_dir pkgtype pkgfile rolefile
+    ostree_dir="$1"
+    for pkgtype in "${pkgtypes[@]}"; do
+        for suff in "" "-$distro" "-${distro}-${major_ver}" "-${distro}-${ver}"; do
+            pkgfile="$ostree_dir/packages-${pkgtype}${suff}.txt"
+            if [ -f "$pkgfile" ]; then
+                cat "$pkgfile"
+            fi
+        done
+        rolefile="$ostree_dir/roles-${pkgtype}.txt"
+        if [ -f "$rolefile" ]; then
+            local roles role rolepath
+            roles="$(cat "$rolefile")"
+            for role in $roles; do
+                rolepath="$(get_rolepath "$ostree_dir" "$role")"
+                get_packages "$rolepath"
+            done
+        fi
+    done | sort -u
+}
+
+format_packages_json() {
+    local comma pkgs pkg
+    comma=""
+    pkgs="["
+    while read -r pkg; do
+        pkgs="${pkgs}${comma}\"${pkg}\""
+        comma=,
+    done
+    pkgs="${pkgs}]"
+    echo "$pkgs"
+}
+
+format_packages_raw() {
+    cat
+}
+
+format_packages_yaml() {
+    while read -r pkg; do
+        echo "- $pkg"
+    done
+}
+
+format_packages_toml() {
+    while read -r pkg; do
+        echo "[[packages]]"
+        echo "name = \"$pkg\""
+        echo "version = \"*\""
+    done
+}
+
+distro="${distro_ver%%-*}"
+ver="${distro_ver##*-}"
+if [[ "$ver" =~ ^([0-9]*) ]]; then
+    major_ver="${BASH_REMATCH[1]}"
+else
+    echo ERROR: cannot parse major version number from version "$ver"
+    exit 1
+fi
+
+"get_$category" "$ostree_dir" | "format_${category}_$format"
diff --git a/.ostree/packages-runtime.txt b/.ostree/packages-runtime.txt
new file mode 100644
index 0000000..933fd5d
--- /dev/null
+++ b/.ostree/packages-runtime.txt
@@ -0,0 +1,4 @@
+authselect
+cockpit-session-recording
+sssd
+tlog
diff --git a/.ostree/packages-testing.txt b/.ostree/packages-testing.txt
new file mode 100644
index 0000000..c3153ef
--- /dev/null
+++ b/.ostree/packages-testing.txt
@@ -0,0 +1 @@
+cockpit
diff --git a/.sanity-ansible-ignore-2.12.txt b/.sanity-ansible-ignore-2.12.txt
new file mode 100644
index 0000000..5309188
--- /dev/null
+++ b/.sanity-ansible-ignore-2.12.txt
@@ -0,0 +1 @@
+roles/tlog/.ostree/get_ostree_data.sh shebang!skip
diff --git a/.sanity-ansible-ignore-2.13.txt b/.sanity-ansible-ignore-2.13.txt
new file mode 100644
index 0000000..5309188
--- /dev/null
+++ b/.sanity-ansible-ignore-2.13.txt
@@ -0,0 +1 @@
+roles/tlog/.ostree/get_ostree_data.sh shebang!skip
diff --git a/.sanity-ansible-ignore-2.14.txt b/.sanity-ansible-ignore-2.14.txt
new file mode 100644
index 0000000..5309188
--- /dev/null
+++ b/.sanity-ansible-ignore-2.14.txt
@@ -0,0 +1 @@
+roles/tlog/.ostree/get_ostree_data.sh shebang!skip
diff --git a/.sanity-ansible-ignore-2.15.txt b/.sanity-ansible-ignore-2.15.txt
new file mode 100644
index 0000000..5309188
--- /dev/null
+++ b/.sanity-ansible-ignore-2.15.txt
@@ -0,0 +1 @@
+roles/tlog/.ostree/get_ostree_data.sh shebang!skip
diff --git a/.sanity-ansible-ignore-2.9.txt b/.sanity-ansible-ignore-2.9.txt
new file mode 100644
index 0000000..5309188
--- /dev/null
+++ b/.sanity-ansible-ignore-2.9.txt
@@ -0,0 +1 @@
+roles/tlog/.ostree/get_ostree_data.sh shebang!skip
diff --git a/README-ostree.md b/README-ostree.md
new file mode 100644
index 0000000..a9f0185
--- /dev/null
+++ b/README-ostree.md
@@ -0,0 +1,66 @@
+# rpm-ostree
+
+The role supports running on [rpm-ostree](https://coreos.github.io/rpm-ostree/)
+systems. The primary issue is that the `/usr` filesystem is read-only, and the
+role cannot install packages. Instead, it will just verify that the necessary
+packages and any other `/usr` files are pre-installed. The role will change the
+package manager to one that is compatible with `rpm-ostree` systems.
+
+## Building
+
+To build an ostree image for a particular operating system distribution and
+version, use the script `.ostree/get_ostree_data.sh` to get the list of
+packages. If the role uses other system roles, then the script will include the
+packages for the other roles in the list it outputs.  The list of packages will
+be sorted in alphanumeric order.
+
+Usage:
+
+```bash
+.ostree/get_ostree_data.sh packages runtime DISTRO-VERSION FORMAT
+```
+
+`DISTRO-VERSION` is in the format that Ansible uses for `ansible_distribution`
+and `ansible_distribution_version` - for example, `Fedora-38`, `CentOS-8`,
+`RedHat-9.4`
+
+`FORMAT` is one of `toml`, `json`, `yaml`, `raw`
+
+* `toml` - each package in a TOML `[[packages]]` element
+
+```toml
+[[packages]]
+name = "package-a"
+version = "*"
+[[packages]]
+name = "package-b"
+version = "*"
+...
+```
+
+* `yaml` - a YAML list of packages
+
+```yaml
+- package-a
+- package-b
+...
+```
+
+* `json` - a JSON list of packages
+
+```json
+["package-a","package-b",...]
+```
+
+* `raw` - a plain text list of packages, one per line
+
+```bash
+package-a
+package-b
+...
+```
+
+What format you choose depends on which image builder you are using.  For
+example, if you are using something based on
+[osbuild-composer](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/composing_installing_and_managing_rhel_for_edge_images/index#creating-an-image-builder-blueprint-for-a-rhel-for-edge-image-using-the-command-line-interface_composing-a-rhel-for-edge-image-using-image-builder-command-line),
+you will probably want to use the `toml` output format.
diff --git a/meta/collection-requirements.yml b/meta/collection-requirements.yml
index afc836d..3e9698f 100644
--- a/meta/collection-requirements.yml
+++ b/meta/collection-requirements.yml
@@ -1,3 +1,5 @@
 ---
 collections:
+  - name: ansible.posix
+  - name: ansible.utils
   - name: community.general
diff --git a/tasks/set_vars.yml b/tasks/set_vars.yml
index 60b6356..406f6c9 100644
--- a/tasks/set_vars.yml
+++ b/tasks/set_vars.yml
@@ -5,6 +5,24 @@
   when: __tlog_required_facts |
     difference(ansible_facts.keys() | list) | length > 0
 
+- name: Ensure correct package manager for ostree systems
+  vars:
+    ostree_pkg_mgr: ansible.posix.rhel_rpm_ostree
+    ostree_booted_file: /run/ostree-booted
+  when: ansible_facts.pkg_mgr | d("") != ostree_pkg_mgr
+  block:
+    - name: Check if system is ostree
+      stat:
+        path: "{{ ostree_booted_file }}"
+      register: __ostree_booted_stat
+
+    - name: Set package manager to use for ostree
+      ansible.utils.update_fact:
+        updates:
+          - path: ansible_facts.pkg_mgr
+            value: "{{ ostree_pkg_mgr }}"
+      when: __ostree_booted_stat.stat.exists
+
 - name: Set platform/version specific variables
   include_vars: "{{ __vars_file }}"
   loop:
diff --git a/tests/tasks/setup.yml b/tests/tasks/setup.yml
new file mode 100644
index 0000000..cb7cd00
--- /dev/null
+++ b/tests/tasks/setup.yml
@@ -0,0 +1,24 @@
+---
+# common test setup tasks
+- name: Check if system is ostree
+  stat:
+    path: "{{ ostree_booted_file }}"
+  register: __ostree_booted_stat
+  vars:
+    ostree_booted_file: /run/ostree-booted
+
+- name: Skip test if not supported on ostree
+  meta: end_host
+  when:
+    - __ostree_booted_stat.stat.exists
+    - __tlog_unsupported_ostree | d(false)
+
+- name: Ensure sssd user/group exist in /etc files
+  shell: |
+    if ! grep -q ^sssd /etc/passwd && grep -q ^sssd /usr/lib/passwd; then
+      grep ^sssd /usr/lib/passwd >> /etc/passwd
+    fi
+    if ! grep -q ^sssd /etc/group && grep -q ^sssd /usr/lib/group; then
+      grep ^sssd /usr/lib/group >> /etc/group
+    fi
+  when: __ostree_booted_stat.stat.exists
diff --git a/tests/tests_cockpit.yml b/tests/tests_cockpit.yml
index 0a229b7..1a6d0cf 100644
--- a/tests/tests_cockpit.yml
+++ b/tests/tests_cockpit.yml
@@ -2,6 +2,11 @@
 - name: Test support for autoinstall of cockpit-session-recording
   hosts: all
   tasks:
+    - name: Test setup and check for ostree
+      include_tasks: tasks/setup.yml
+      vars:
+        __tlog_unsupported_ostree: true
+
     - name: Get the rpm package facts
       package_facts:
 
diff --git a/tests/tests_default.yml b/tests/tests_default.yml
index 4665b5e..37fd6f3 100644
--- a/tests/tests_default.yml
+++ b/tests/tests_default.yml
@@ -1,7 +1,9 @@
 ---
 - name: Ensure that the role runs with default parameters
   hosts: all
-
   roles:
     - linux-system-roles.tlog
   gather_facts: false
+  pre_tasks:
+    - name: Test setup and check for ostree
+      include_tasks: tasks/setup.yml
diff --git a/tests/tests_default_vars.yml b/tests/tests_default_vars.yml
index 93f5075..4c73c30 100644
--- a/tests/tests_default_vars.yml
+++ b/tests/tests_default_vars.yml
@@ -3,6 +3,9 @@
   hosts: all
   roles:
     - linux-system-roles.tlog
+  pre_tasks:
+    - name: Test setup and check for ostree
+      include_tasks: tasks/setup.yml
   tasks:
     - name: Check that all variables are defined
       assert:
diff --git a/tests/tests_example.yml b/tests/tests_example.yml
index 391fc06..f60b308 100644
--- a/tests/tests_example.yml
+++ b/tests/tests_example.yml
@@ -7,3 +7,6 @@
     tlog_scope_sssd: some
     tlog_users_sssd:
       - recordeduser
+  pre_tasks:
+    - name: Test setup and check for ostree
+      include_tasks: tasks/setup.yml
diff --git a/tests/tests_sssd.yml b/tests/tests_sssd.yml
index 5c2b20a..6b0c785 100644
--- a/tests/tests_sssd.yml
+++ b/tests/tests_sssd.yml
@@ -3,6 +3,9 @@
   hosts: all
 
   tasks:
+    - name: Test setup and check for ostree
+      include_tasks: tasks/setup.yml
+
     - name: Run role with default sssd settings
       import_role:
         name: linux-system-roles.tlog