From 68bca84481384c7d2aa5b695d1751a953e3ef44e Mon Sep 17 00:00:00 2001
From: Rafael Guterres Jeffman <rjeffman@redhat.com>
Date: Wed, 26 Jun 2024 14:46:53 +0000
Subject: [PATCH] utils: Rewrite run-tests.sh to use functions and extenal
 scripts

To modify Azure tests and depend on shell scripts and pytest instead
of molecule, the run-tests.sh script has been rewritten to depend on
bash functions and on a bash script that prepare and start a testing
container.

This patch adds a new script, 'utils/setup_test_container.sh' that
can be used to start a new container, using either podman or docker,
based on the available ansible-freeipa images. The new container can
then be used to run ansible-freeipa tests against it.

Also the following files with bash functions were added, and are
used by both scripts:

    utils/shansible: Functions to run playbooks in the container
    utils/shcontainer: Functions to setup/run a container
    utils/shfun: Generic shell helper functions (e.g.: log)
---
 utils/run-tests.sh            | 363 ++++++----------------------------
 utils/set_test_modules        |   4 +-
 utils/setup_test_container.sh | 119 +++++++++++
 utils/shansible               |  88 +++++++++
 utils/shcontainer             |  61 ++++++
 5 files changed, 336 insertions(+), 299 deletions(-)
 create mode 100755 utils/setup_test_container.sh
 create mode 100644 utils/shansible
 create mode 100644 utils/shcontainer

diff --git a/utils/run-tests.sh b/utils/run-tests.sh
index e814aed0..140998be 100755
--- a/utils/run-tests.sh
+++ b/utils/run-tests.sh
@@ -1,38 +1,19 @@
 #!/bin/bash -eu
 
-trap interrupt_exception SIGINT
+SCRIPTDIR="$(readlink -f "$(dirname "$0")")"
+TOPDIR="$(readlink -f "${SCRIPTDIR}/..")"
 
-RST="\033[0m"
-RED="\033[31m"
-# BRIGHTRED="\033[31;1m"
-# GREEN="\033[32m"
-BRIGHTGREEN="\033[32;1m"
-# BROWN="\033[33m"
-YELLOW="\033[33;1m"
-# NAVY="\033[34m"
-BLUE="\033[34;1m"
-# MAGENTA="\033[35m"
-# BRIGHTMAGENTA="\033[35;1m"
-# DARKCYAN="\033[36m"
-# CYAN="\033[36;1m"
-# BLACK="\033[30m"
-# DARKGRAY="\033[30;1m"
-# GRAY="\033[37m"
-WHITE="\033[37;1m"
-
-TOPDIR="$(readlink -f "$(dirname "$0")/..")"
-
-interrupt_exception() {
-    trap - SIGINT
-    log warn "User interrupted test execution."
-    cleanup
-    exit 1
-}
+# shellcheck source=utils/shfun
+. "${SCRIPTDIR}/shfun"
+# shellcheck source=utils/shcontainer
+. "${SCRIPTDIR}/shcontainer"
+# shellcheck source=utils/shansible
+. "${SCRIPTDIR}/shansible"
 
 usage() {
     local prog="${0##*/}"
     cat <<EOF
-usage: ${prog} [-h] [-l] [-e] [-K] [-c CONTAINER] [-s TESTS_SUITE] [-x] [-A SEED.GRP] [-i IMAGE] [-m MEMORY] [-v...] [TEST...]
+usage: ${prog} [-h] [-l] [-e] [-K] [-A|-a ANSIBLE] [-p INTERPRETER] [-c CONTAINER] [-s TESTS_SUITE] [-x] [-S SEED.GRP] [-i IMAGE] [-m MEMORY] [-v...] [TEST...]
     ${prog} runs playbook(s) TEST using an ansible-freeipa testing image.
 
 EOF
@@ -47,172 +28,72 @@ positional arguments:
 
 optional arguments:
   -h              display this message and exit
+  -a ANSIBLE      Ansible version to use, e.g. "ansible-core==2.16.0"
+                  (default: latest ansible-core for the python version)
+  -A              Do not install Ansible, use host's provided one.
   -c CONTAINER    use container CONTAINER to run tests
   -K              keep container, even if tests succeed
   -l              list available images
   -e              force recreation of the virtual environment
   -i IMAGE        select image to run the tests (default: fedora-latest)
-  -m              container memory, in GiB (default: 3)
+  -m MEMORY       container memory, in GiB (default: 3)
+  -p INTERPRETER  Python interpreter to use on target container
   -s TEST_SUITE   run all playbooks for test suite, which is a directory
                   under ${WHITE}tests${RST}
-  -A SEED.GROUP   Replicate Azure's test group and seed (seed is YYYYMMDD)
+  -S SEED.GROUP   Replicate Azure's test group and seed (seed is YYYYMMDD)
   -v              Increase Ansible verbosity (can be used multiple times)
   -x              Stop on first error.
 EOF
 )"
 }
 
-log() {
-    local level="${1^^}" message="${*:2}"
-    case "${level}" in
-        ERROR) COLOR="${RED}" ;;
-        WARN)  COLOR="${YELLOW}" ;;
-        DEBUG) COLOR="${BLUE}" ;;
-        INFO) COLOR="${WHITE}" ;;
-        SUCCESS) COLOR="${BRIGHTGREEN}" ;;
-        *) COLOR="${RST}" ;;
-    esac
-    echo -en "${COLOR}"
-    [ "${level}" == "ERROR" ] && echo -en "${level}:"
-    echo -e "${message}${RST}"
-}
-
-quiet() {
-     "$@" >/dev/null 2>&1
-}
-
-in_python_virtualenv() {
-    local script
-    read -r -d "" script <<EOS
-import sys;
-base = getattr(sys, "base_prefix", ) or getattr(sys, "real_prefix", ) or sys.prefix
-print('yes' if sys.prefix != base else 'no')
-EOS
-    test "$(python -c "${script}")" == "yes"
-}
-
-run_inline_playbook() {
-    local playbook
-    local err
-    quiet mkdir -p "${test_env}/playbooks"
-    playbook=$(mktemp "${test_env}/playbooks/ansible-freeipa-test-playbook_ipa.XXXXXXXX")
-    cat - >"${playbook}"
-    ansible-playbook -i "${inventory}" "${playbook}"
-    err=$?
-    rm "${playbook}"
-    return ${err}
-}
-
-die() {
-    usg="N"
-    if [ "${1}" == "-u" ]
-    then
-       usg="Y"
-       shift 1
-    fi
-    log error "${*}"
-    STOP_CONTAINER="N"
-    cleanup
-    [ "${usg}" == "Y" ] && usage
-    exit 1
-}
-
-make_inventory() {
-    local scenario=$1 engine=${2:-podman}
-    inventory="${test_env}/inventory"
-    log info "Inventory file: ${inventory}"
-    cat << EOF > "${inventory}"
-[ipaserver]
-${scenario} ansible_connection=${engine}
-[ipaserver:vars]
-ipaserver_domain = test.local
-ipaserver_realm = TEST.LOCAL
-EOF
-}
-
-stop_container() {
-    local scenario=${1} engine=${2:-podman}
-    echo "Stopping container..."
-    quiet "${engine}" stop "${scenario}"
-    echo "Removing container..."
-    quiet "${engine}" rm "${scenario}"
-}
-
-cleanup() {
-    if [ $# -gt 0 ]
-    then
-        if [ "${STOP_CONTAINER}" != "N" ]
-        then
-            stop_container "${1}" "${2}"
-            rm "${inventory}"
-        else
-            log info "Keeping container: $(podman ps --format "{{.Names}} - {{.ID}}" --filter "name=${1}")"
-        fi
-    fi
-    if [ "${STOP_VIRTUALENV}" == "Y" ]
-    then
-        echo "Deactivating virtual environment"
-        deactivate
-    fi
-}
-
-list_images() {
-    local quay_api="https://quay.io/api/v1/repository/ansible-freeipa/upstream-tests/tag"
-    echo -e "${WHITE}Available images:"
-    curl --silent -L "${quay_api}" | jq '.tags[]|.name' | tr -d '"'| sort | uniq | sed "s/.*/    &/"
-    echo -e "${RST}"
-}
 
 # Defaults
-
-ANSIBLE_VERSION=${ANSIBLE_VERSION:-'ansible-core'}
 verbose=""
-FORCE_ENV="N"
+engine="${engine:-"podman"}"
 CONTINUE_ON_ERROR=""
 STOP_CONTAINER="Y"
 STOP_VIRTUALENV="N"
 declare -a ENABLED_MODULES
 declare -a ENABLED_TESTS
-ENABLED_MODULES=()
-ENABLED_TESTS=()
-test_env="${TESTENV_DIR:-${VIRTUAL_ENV:-/tmp/ansible-freeipa-tests}}"
-engine="podman"
-IMAGE_REPO="quay.io/ansible-freeipa/upstream-tests"
+read -r -a ENABLED_MODULES <<< "${IPA_ENABLED_MODULES:-""}"
+read -r -a ENABLED_TESTS <<< "${IPA_ENABLED_MODULES:-""}"
 IMAGE_TAG="fedora-latest"
-scenario=""
+scenario="freeipa-tests"
 MEMORY=3
-hostname="ipaserver.test.local"
-SEED=""
-GROUP=0
+IPA_HOSTNAME="ipaserver.test.local"
+SEED="$(date "+%Y%m%d")"
+GROUP=1
 SPLITS=0
-ANSIBLE_COLLECTIONS=${ANSIBLE_COLLECTIONS:-"containers.podman"}
-
+ANSIBLE_COLLECTIONS=${ANSIBLE_COLLECTIONS:-"${engine_collection}"}
+SKIP_ANSIBLE=""
+ansible_interpreter="/usr/bin/python3"
 EXTRA_OPTIONS=""
+unset ANSIBLE_VERSION
 
 # Process command options
 
-while getopts ":hA:c:ei:Klms:vx" option
+while getopts ":ha:Ac:ei:Klm:p:s:S:vx" option
 do
     case "$option" in
         h) help && exit 0 ;;
         A)
-            [ ${#ENABLED_MODULES[@]} -eq 0 ] || die -u "Can't use '-A' with '-s'"
-            SEED="$(cut -d. -f1 <<< "${OPTARG}" | tr -d "-")"
-            GROUP="$(cut -d. -f2 <<< "${OPTARG}")"
-            if [ -z "${SEED}" ] || [ -z "${GROUP}" ]
-            then
-                die -u "Seed for '-A' must have the format YYYYMMDD.N"
-            fi
-            SPLITS=3
-        ;;
+           [ -n "${ANSIBLE_VERSION:-""}" ] && die "Can't use -A with '-a'"
+           SKIP_ANSIBLE="YES"
+           ;;
+        a) 
+           [ "${SKIP_ANSIBLE:-"no"}" == "YES" ] && die "Can't use -A with '-a'"
+           ANSIBLE_VERSION="${OPTARG}"
+           ;;
         c) scenario="${OPTARG}" ;;
         e) FORCE_ENV="Y" ;;
         i) IMAGE_TAG="${OPTARG}" ;;
         K) STOP_CONTAINER="N" ;;
-        l) list_images && exit 0 || exit 1;;
+        l) "${SCRIPTDIR}"/setup_test_container.sh -l && exit 0 || exit 1 ;;
         m) MEMORY="${OPTARG}" ;;
+        p) ansible_interpreter="${OPTARG}" ;;
         s)
-           [ ${SPLITS} -ne 0 ] && die -u "Can't use '-A' with '-s'"
+           [ ${SPLITS} -ne 0 ] && die -u "Can't use '-S' with '-s'"
            if [ -d "${TOPDIR}/tests/${OPTARG}" ]
            then
                ENABLED_MODULES+=("${OPTARG}")
@@ -220,6 +101,16 @@ do
                log error "Invalid suite: ${OPTARG}"
            fi
            ;;
+        S)
+           [ ${#ENABLED_MODULES[@]} -eq 0 ] || die -u "Can't use '-A' with '-s'"
+           SEED="$(cut -d. -f1 <<< "${OPTARG}" | tr -d "-")"
+           GROUP="$(cut -d. -f2 <<< "${OPTARG}")"
+           if [ -z "${SEED}" ] || [ -z "${GROUP}" ]
+           then
+               die -u "Seed for '-A' must have the format YYYYMMDD.N"
+           fi
+           SPLITS=3
+           ;;
         v) verbose=${verbose:--}${option} ;;
         x) EXTRA_OPTIONS="$EXTRA_OPTIONS --exitfirst" ;;
         *) die -u "Invalid option: ${OPTARG}" ;;
@@ -240,155 +131,28 @@ done
 
 [ ${SPLITS} -eq 0 ] && [ ${#ENABLED_MODULES[@]} -eq 0 ] && [ ${#ENABLED_TESTS[@]} -eq 0 ] && die -u "No test defined."
 
-# Prepare virtual environment
-VENV=$(in_python_virtualenv && echo Y || echo N)
+export STOP_CONTAINER FORCE_ENV STOP_VIRTUALENV ansible_interpreter
 
-if [ "${FORCE_ENV}" == "Y" ]
-then
-    [ "${VENV}" == "Y" ] && deactivate
-    VENV="N"
-    rm -rf "$test_env"
-    log info "Virtual environment will be (re)created."
-fi
+# Ensure $python is set
+[ -z "${python}" ] && python="python3"
 
-if [ "$VENV" == "N" ]
-then
-    log info "Preparing virtual environment: ${test_env}"
-    if [ ! -d "${test_env}" ]
-    then
-        log info "Creating virtual environment: ${test_env}..."
-        if ! python3 -m venv "${test_env}"
-        then
-            die "Cannot create virtual environment."
-        fi
-    fi
-    if [ -f "${test_env}/bin/activate" ]
-    then
-        log info "Starting virtual environment: ${test_env}"
-        # shellcheck disable=SC1091
-        . "${test_env}/bin/activate" || die "Cannot activate environment."
-        STOP_VIRTUALENV="Y"
-    else
-        die "Cannot activate environment."
-    fi
-    log info "Installing required tools."
-    log none "Upgrading: pip setuptools wheel"
-    pip install --quiet --upgrade pip setuptools wheel
-    log info "Installing dependencies from 'requirements-tests.txt'"
-    pip install --quiet -r "${TOPDIR}/requirements-tests.txt"
-    log info "Installing Ansible: ${ANSIBLE_VERSION}"
-    pip install --quiet "${ANSIBLE_VERSION}"
-    log debug "Ansible version: $(ansible --version | sed -n "1p")${RST}"
-else
-   log info "Using current virtual environment."
-fi
+log info "Controller Python executable: ${python}"
+${python} --version
 
-if [ -n "${ANSIBLE_COLLECTIONS}" ]
-then
-    log warn "Installed collections will not be removed after execution."
-    log none "Installing: Ansible Collection ${ANSIBLE_COLLECTIONS}"
-    # shellcheck disable=SC2086
-    quiet ansible-galaxy collection install ${ANSIBLE_COLLECTIONS} || die "Failed to install Ansible collections."
-fi
+# Prepare virtual environment
+start_virtual_environment
+log info "Installing dependencies from 'requirements-tests.txt'"
+pip install --upgrade -r "${TOPDIR}/requirements-tests.txt"
+
+[ -z "${SKIP_ANSIBLE}" ] && install_ansible "${ANSIBLE_VERSION:-"ansible-core"}"
 
 # Ansible configuration
 export ANSIBLE_ROLES_PATH="${TOPDIR}/roles"
-export ANSIBLE_LIBRARY="${TOPDIR}/plugins:${TOPDIR}/molecule"
+export ANSIBLE_LIBRARY="${TOPDIR}/plugins"
 export ANSIBLE_MODULE_UTILS="${TOPDIR}/plugins/module_utils"
 
-# Prepare container
-container_id=""
-container_status=("-f" "status=created" "-f" "status=running")
-[ -n "${scenario}" ] && container_id="$(${engine} ps --all -q -f "name=${scenario}" "${container_status[@]}")"
-if [ -z "${container_id}" ]
-then
-    # Retrieve image and start container.
-    log info "Pulling FreeIPA image '${IMAGE_REPO}:${IMAGE_TAG}'..."
-    img_id=$(${engine} pull -q "${IMAGE_REPO}:${IMAGE_TAG}")
-    log info "Creating container..."
-    CONFIG="--hostname ${hostname} --memory ${MEMORY}g --memory-swap -1 --dns none --add-host ipaserver.test.local:127.0.0.1"
-    [ -n "${scenario}" ] && CONFIG="${CONFIG} --name ${scenario}"
-    # shellcheck disable=SC2086
-    container_id=$(${engine} create ${CONFIG} "${img_id}" || die "Cannot create container")
-    echo "CONTAINER: ${container_id}"
-fi
-scenario="${scenario:-$(${engine} ps -q --format "{{.Names}}" --filter "id=${container_id}" "${container_status[@]}")}"
-log debug "Using container: ${scenario}"
-
 # Start container
-make_inventory "${scenario}"
-log info "Starting container for ${scenario}..."
-quiet ${engine} start "${scenario}"
-
-# create /etc/resolve.conf
-run_inline_playbook <<EOF || die "Failed to create /etc/resolv.conf"
----
-- name: Create /etc/resolv.conf
-  hosts: ipaserver
-  gather_facts: no
-  become: yes
-  tasks:
-  - name: Create /etc/resolv.conf
-    ansible.builtin.copy:
-      dest: /etc/resolv.conf
-      mode: 0644
-      content: |
-        search test.local
-        nameserver 127.0.0.1
-...
-EOF
-
-# wait for FreeIPA services to be available
-run_inline_playbook <<EOF || die "Failed to verify IPA or KDC services."
----
-- name: Wait for IPA services to be available
-  hosts: ipaserver
-  gather_facts: no
-  tasks:
-  - name: Wait for IPA to be started.
-    ansible.builtin.systemd:
-      name: ipa
-      state: started
-  - name: Wait for Kerberos KDC to be started.
-    ansible.builtin.systemd:
-      name: krb5kdc
-      state: started
-    register: result
-    until: not result.failed
-    retries: 30
-    delay: 5
-  - name: Check if TGT is available for admin.
-    ansible.builtin.shell:
-      cmd: echo SomeADMINpassword | kinit -c ansible_freeipa_cache admin
-    register: result
-    until: not result.failed
-    retries: 30
-    delay: 5
-  - name: Cleanup TGT.
-    ansible.builtin.shell:
-      cmd: kdestroy -c ansible_freeipa_cache -A
-...
-EOF
-
-# check image software versions.
-run_inline_playbook <<EOF || die "Failed to verify software installation."
----
-- name: Software environment.
-  hosts: ipaserver
-  become: yes
-  gather_facts: no
-  tasks:
-  - name: Retrieve versions.
-    shell:
-      cmd: |
-        rpm -q freeipa-server freeipa-client ipa-server ipa-client 389-ds-base pki-ca krb5-server
-        cat /etc/redhat-release
-        uname -a
-    register: result
-  - name: Testing environment.
-    debug:
-      var: result.stdout_lines
-EOF
+"${SCRIPTDIR}/setup_test_container.sh" -e "${engine}" -m "${MEMORY}" -p "${ansible_interpreter}" -i "${IMAGE_TAG}" -n "${IPA_HOSTNAME}" -a "${scenario}" || die "Failed to setup test container"
 
 
 # run tests
@@ -396,6 +160,9 @@ RESULT=0
 
 export RUN_TESTS_IN_DOCKER=${engine}
 export IPA_SERVER_HOST="${scenario}"
+# Ensure proper ansible_python_interpreter is used by pytest.
+export IPA_PYTHON_PATH="${ansible_interpreter}"
+
 if [ ${SPLITS} -ne 0 ]
 then
     EXTRA_OPTIONS="${EXTRA_OPTIONS} --splits=${SPLITS} --group=${GROUP} --randomly-seed=${SEED}"
@@ -417,11 +184,11 @@ IPA_VERBOSITY="${verbose}"
 [ -n "${IPA_VERBOSITY}" ] && export IPA_VERBOSITY
 
 # shellcheck disable=SC2086
-if ! pytest -m "playbook" --verbose --color=yes ${EXTRA_OPTIONS}
+if ! pytest -m "playbook" --verbose --color=yes --suppress-no-test-exit-code --junit-xml=TEST-results-group-${GROUP:-1}.xml ${EXTRA_OPTIONS}
 then
     RESULT=2
     log error "Container not stopped for verification: ${scenario}"
-    log info "Container: $(podman ps -f "id=${container_id}" --format "{{.Names}} - {{.ID}}")"
+    log info "Container: $(${engine} ps -f "name=${scenario}" --format "{{.Names}} - {{.ID}}")"
 fi
 [ -z "${CONTINUE_ON_ERROR}" ] && [ $RESULT -ne 0 ] && die "Stopping on test failure."
 
diff --git a/utils/set_test_modules b/utils/set_test_modules
index 9f94d2c7..daa47dcf 100644
--- a/utils/set_test_modules
+++ b/utils/set_test_modules
@@ -14,6 +14,8 @@ die() {
 
 TOPDIR="$(dirname "${BASH_SOURCE[0]}")/.."
 
+[ -n "$(command -v python3)" ] && python="$(command -v python3)" || python="$(command -v python2)"
+
 pushd "${TOPDIR}" >/dev/null 2>&1 || die "Failed to change directory."
 
 files_list=$(mktemp)
@@ -25,7 +27,7 @@ git diff "${remote}/master" --name-only > "${files_list}"
 git remote remove ${remote}
 
 # Get all modules that should have tests executed
-enabled_modules="$(python utils/get_test_modules.py $(cat "${files_list}"))"
+enabled_modules="$(${python} utils/get_test_modules.py $(cat "${files_list}"))"
 [ -z "${enabled_modules}" ] && enabled_modules="None"
 
 # Get individual tests that should be executed
diff --git a/utils/setup_test_container.sh b/utils/setup_test_container.sh
new file mode 100755
index 00000000..0916c27f
--- /dev/null
+++ b/utils/setup_test_container.sh
@@ -0,0 +1,119 @@
+#!/bin/bash -eu
+
+SCRIPTDIR="$(readlink -f "$(dirname "$0")")"
+
+# shellcheck source=utils/shcontainer
+. "${SCRIPTDIR}/shcontainer"
+# shellcheck source=utils/shansible
+. "${SCRIPTDIR}/shansible"
+
+usage() {
+    local prog="${0##*/}"
+    cat <<EOF
+usage: ${prog} [-h] [-l] [-a] [-e ENGINE] [-i IMAGE] [-m MEMORY] [-n HOSTNAME] NAME
+    ${prog} starts a container to test ansible-freeipa.
+
+EOF
+}
+
+help() {
+    usage
+    echo -e "$(cat <<EOF
+Arguments:
+
+  NAME            set the container name
+
+Options:
+
+  -h              display this message and exit
+  -l              list available images
+  -a              Test Ansible connection.
+  -e ENGINE       set the container engine to use
+                  (default: ${WHITE}podman${RST}, if available)
+  -i IMAGE        select image to run the tests (default: fedora-latest)
+  -m MEMORY       set container memory, in GiB (default: 3)
+  -n HOSTNAME     set the hostname in the container
+                  (default: ipaserver.test.local)
+  -p INTERPRETER  Python interpreter to use on target container
+EOF
+)"
+}
+
+list_images() {
+    local quay_api="https://quay.io/api/v1/repository/ansible-freeipa/upstream-tests/tag"
+    echo -e "${WHITE}Available images:"
+    curl --silent -L "${quay_api}" | jq '.tags[]|.name' | tr -d '"'| sort | uniq | sed "s/.*/    &/"
+    echo -e "${RST}"
+}
+
+IMAGE_TAG="fedora-latest"
+MEMORY="${MEMORY:-3}"
+IPA_HOSTNAME="${IPA_HOSTNAME:-"ipaserver.test.local"}"
+test_env="${test_env:-"/tmp"}"
+ansible_interpreter="/usr/bin/python3"
+engine="podman"
+ansible_test=""
+
+while getopts ":hae:i:lm:n:p:" option
+do
+    case "$option" in
+        h) help && exit 0 ;;
+        a) ansible_test="yes" ;;
+        e) engine="${OPTARG}" ;;
+        i) IMAGE_TAG="${OPTARG}" ;;
+        l) list_images && exit 0 || exit 1;;
+        m) MEMORY="${OPTARG}" ;;
+        n) IPA_HOSTNAME="${OPTARG}" ;;
+        p) ansible_interpreter="${OPTARG}" ;;
+        *) die -u "Invalid option: ${OPTARG}" ;;
+    esac
+done
+
+export IPA_HOSTNAME MEMORY IMAGE_TAG scenario
+
+shift $((OPTIND - 1))
+[ $# == 1 ] || die -u "You must provide the name for a single container." 
+scenario="${1}"
+shift
+
+prepare_container "${scenario}" "${IMAGE_TAG}"
+start_container "${scenario}"
+
+# wait for FreeIPA services to be available (usually ~45 seconds)
+log info "Wait for container to be initialized."
+wait=15
+while podman exec "${scenario}" systemctl list-jobs | grep -qvi "no jobs running"
+do
+    log none "Waiting ${wait}s... "
+    sleep "${wait}"
+    log none "Retrying".
+done
+
+# run tests
+
+# ensure we can get a TGT for admin
+log info "Testing kinit with admin."
+# shellcheck disable=SC2016
+"${engine}" exec "${scenario}" /bin/sh -c 'for i in $(seq 5); do echo "SomeADMINpassword" | kinit -c ansible_freeipa_cache admin && kdestroy -c ansible_freeipa_cache -A && break; echo "Failed to get TGT. Retrying in 10s..."; sleep 10; done' || die "Failed to grant admin TGT."
+
+# shellcheck disable=SC2154
+log info "Creating inventory."
+make_inventory "${scenario}" "${engine}" "${ansible_interpreter:-"/usr/bin/python3"}"
+if [ -z "${inventory:-''}" ]
+then
+    log error "Could not create inventory file."
+else
+    # shellcheck disable=SC2154
+    log info "Inventory path: [${inventory}]"
+    # shellcheck disable=SC2154
+    log debug "$(cat "${inventory}")"
+    if [ "${ansible_test}" == "yes" ]
+    then
+        log info "Testing Ansible connection."
+        # shellcheck disable=SC2154
+        run_if_exists ansible_ping "${inventory}"
+        log info "Querying installed software"
+        run_if_exists query_container_installed_software
+    fi
+fi
+
diff --git a/utils/shansible b/utils/shansible
new file mode 100644
index 00000000..ca05f820
--- /dev/null
+++ b/utils/shansible
@@ -0,0 +1,88 @@
+#!/bin/bash -eu
+# This file is meant to be source'd by other scripts
+
+SCRIPTDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
+
+# shellcheck source=utils/shfun
+. "${SCRIPTDIR}/shfun"
+
+install_ansible() {
+    ANSIBLE_VERSION="${1:-${ANSIBLE_VERSION:-"ansible-core"}}"
+    [ $# -gt 0 ] && shift
+    log info "Installing Ansible: ${ANSIBLE_VERSION}"
+    pip install --quiet "${ANSIBLE_VERSION}"
+    log debug "Ansible version: $(ansible --version | sed -n "1p")${RST}"
+
+    if [ -n "${ANSIBLE_COLLECTIONS}" ]
+    then
+        log warn "Installed collections will not be removed after execution."
+        log none "Installing: Ansible Collection ${ANSIBLE_COLLECTIONS}"
+        # shellcheck disable=SC2086
+        quiet ansible-galaxy collection install ${ANSIBLE_COLLECTIONS} || die "Failed to install Ansible collections."
+    fi
+    export ANSIBLE_VERSION
+}
+
+run_inline_playbook() {
+    local playbookdir playbook err
+    playbookdir=${1:-"playbooks"}
+    quiet mkdir -p "${playbookdir}"
+    playbook=$(mktemp "${playbookdir}/ansible-freeipa-test-playbook_ipa.XXXXXXXX" 2>/dev/null)
+    # In some configurations, it may not be possible to use another
+    # directory, so we store the playbook in the current one.
+    # [ -z "${playbook}" ] && playbook=$(mktemp "ansible-freeipa-test-playbook_ipa.XXXXXXXX")
+
+    inventory="${inventory:-${test_env:-"."}/inventory}"
+    quiet mkdir -p "${playbookdir}"
+    cat - >"${playbook}"
+    # shellcheck disable=SC2086
+    run_if_exists ansible-playbook ${ansible_options:-} -i "${inventory}" "${playbook}"
+    err=$?
+    rm -f "${playbook}"
+    return ${err}
+}
+
+make_inventory() {
+    local scenario pod_engine ansible_interpreter
+    scenario=$1
+    pod_engine="${engine:-${2:-podman}}"
+    ansible_interpreter="${3:-${ansible_interpreter:-"/usr/bin/python3"}}"
+    export inventory="${test_env:-"."}/inventory"
+    log info "Inventory file: ${inventory}"
+    cat << EOF > "${inventory}"
+[ipaserver]
+${scenario} ansible_connection=${pod_engine} ansible_python_interpreter=${ansible_interpreter}
+[ipaserver:vars]
+ipaserver_domain = test.local
+ipaserver_realm = TEST.LOCAL
+EOF
+}
+
+query_container_installed_software() {
+    # check image software versions.
+    run_inline_playbook "${test_env:-"/tmp"}/playbooks" <<EOF || die "Failed to verify software installation."
+---
+- name: Software environment.
+  hosts: ipaserver
+  become: yes
+  gather_facts: no
+  tasks:
+  - name: Retrieve versions.
+    ansible.builtin.shell: |
+      cat /etc/redhat-release
+      ${python:-"python3"} --version
+      rpm -q freeipa-server freeipa-client ipa-server ipa-client 389-ds-base pki-ca krb5-server
+      uname -a
+    register: result
+  - name: Testing environment.
+    ansible.builtin.debug:
+      var: result.stdout_lines
+EOF
+}
+
+ansible_ping() {
+    # shellcheck disable=SC2086
+    ansible ${ansible_options:-} -m ping -i "${1:-${inventory}}" all || die "Could not connect to container."
+}
+
+export ANSIBLE_VERSION=${ANSIBLE_VERSION:-'ansible-core'}
diff --git a/utils/shcontainer b/utils/shcontainer
new file mode 100644
index 00000000..b1c56830
--- /dev/null
+++ b/utils/shcontainer
@@ -0,0 +1,61 @@
+#!/bin/bash -eu
+# This file is meant to be source'd by other scripts
+
+SCRIPTDIR="$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")"
+
+IMAGE_REPO="quay.io/ansible-freeipa/upstream-tests"
+
+# shellcheck source=utils/shfun
+. "${SCRIPTDIR}/shfun"
+
+stop_container() {
+    local scenario=${1}
+    log none "Stopping container..."
+    quiet "${engine}" stop "${scenario}"
+    log none "Removing container..."
+    quiet "${engine}" rm "${scenario}"
+}
+
+# Prepare container
+prepare_container() {
+    local container_id container_status hostname scenario
+    local IMAGE_TAG img_id CONFIG
+    container_id=""
+    container_status=("-f" "status=created" "-f" "status=running")
+    hostname="${IPA_HOSTNAME:-"ipaserver.test.local"}"
+    scenario="${1:-${scenario:-"freeipa-tests"}}"
+    IMAGE_TAG="${2:-${IMAGE_TAG:-fedora-latest}}"
+    [ -n "${scenario}" ] && container_id="$(${engine} ps --all -q -f "name=${scenario}" "${container_status[@]}")"
+    if [ -z "${container_id}" ]
+    then
+        # Retrieve image and start container.
+        log info "Pulling FreeIPA image '${IMAGE_REPO}:${IMAGE_TAG}'..."
+        img_id=$(${engine} pull -q "${IMAGE_REPO}:${IMAGE_TAG}")
+        log debug "Hostname: ${hostname}"
+        log info "Creating container..."
+        CONFIG="--systemd true --hostname ${hostname} --memory ${MEMORY}g --memory-swap -1 --no-hosts"
+        [ -n "${scenario}" ] && CONFIG="${CONFIG} --name ${scenario}"
+        # shellcheck disable=SC2086
+        container_id=$(${engine} create ${CONFIG} "${img_id}" || die "Cannot create container")
+        log none "CONTAINER: ${container_id}"
+    fi
+    export scenario="${scenario:-$(${engine} ps -q --format "{{.Names}}" --filter "id=${container_id}" "${container_status[@]}")}"
+    log debug "Prepared container: ${scenario}"
+}
+
+start_container() {
+    local scenario="${1:-${scenario}}"
+    log info "Starting container for ${scenario}..."
+    "${engine}" start "${scenario}"
+}
+
+if [ -z "$(command -v podman)" ]
+then
+    engine="docker"
+    engine_collection="community.docker"
+else
+    engine="podman"
+    engine_collection="containers.podman"
+fi
+
+export engine engine_collection
-- 
GitLab