diff --git a/.gitignore b/.gitignore index d74bada6186a53466cebef28d7071db10be7cc82..626d54fa621f3bf410aebd90ea10fd151275cec4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ importer_result.json /.venv/ tests/logs/ +TEST*.xml diff --git a/tests/README.md b/tests/README.md index ed54b0b8221fb2954721c807a13fef6ce4ec4a16..1e9d4e4a194d47b99e2fa94d02202678d3765288 100644 --- a/tests/README.md +++ b/tests/README.md @@ -36,6 +36,12 @@ environment variable. For example: IPA_SSH_PASSWORD=<ipaserver_ssh_password> IPA_SERVER_HOST=<ipaserver_host_or_ip> pytest ``` +If you want, or need to to set the Python interpreter to use, you must set `IPA_PYTHON_PATH` +environment variable. For example: + +``` +IPA_PYTHON_PATH=/usr/bin/python3.14 IPA_SERVER_HOST=<ipaserver_host_or_ip> pytest +``` To run a single test use the full path with the following format: diff --git a/tests/azure/pr-pipeline.yml b/tests/azure/pr-pipeline.yml index 2345347db4a07ad19d7a63a48daa7d41b246f7d2..40d78a9b10429cf19d14d8d7c79a4bffa6cae15d 100644 --- a/tests/azure/pr-pipeline.yml +++ b/tests/azure/pr-pipeline.yml @@ -15,8 +15,8 @@ stages: - template: templates/fast_tests.yml parameters: build_number: $(Build.BuildNumber) - scenario: fedora-latest - ansible_version: "-core >=2.14,<2.15" + distro: fedora-latest + ansible_version: "-core >=2.15,<2.16" # Galaxy on Fedora @@ -26,8 +26,8 @@ stages: - template: templates/fast_tests.yml parameters: build_number: $(Build.BuildNumber) - scenario: fedora-latest - ansible_version: "-core >=2.14,<2.15" + distro: fedora-latest + ansible_version: "-core >=2.15,<2.16" # CentOS 9 Stream @@ -37,8 +37,8 @@ stages: - template: templates/fast_tests.yml parameters: build_number: $(Build.BuildNumber) - scenario: c9s - ansible_version: "-core >=2.14,<2.15" + distro: c9s + ansible_version: "-core >=2.15,<2.16" # CentOS 8 Stream @@ -48,19 +48,23 @@ stages: - template: templates/fast_tests.yml parameters: build_number: $(Build.BuildNumber) - scenario: c8s - ansible_version: "-core >=2.14,<2.15" + distro: c8s + ansible_version: "-core >=2.15,<2.16" + target_python: "/usr/libexec/platform-python" +# CentOS 7 cannot be used with current systemd +# # CentOS 7 - -- stage: CentOS_7 - dependsOn: [] - jobs: - - template: templates/fast_tests.yml - parameters: - build_number: $(Build.BuildNumber) - scenario: centos-7 - ansible_version: "-core >=2.14,<2.15" +# +# - stage: CentOS_7 +# dependsOn: [] +# jobs: +# - template: templates/fast_tests.yml +# parameters: +# build_number: $(Build.BuildNumber) +# distro: centos-7 +# ansible_version: "-core >=2.15,<2.16" +# target_python: "/usr/bin/python2" # Rawhide @@ -70,5 +74,5 @@ stages: - template: templates/fast_tests.yml parameters: build_number: $(Build.BuildNumber) - scenario: fedora-rawhide - ansible_version: "-core >=2.14,<2.15" + distro: fedora-rawhide + ansible_version: "-core >=2.15,<2.16" diff --git a/tests/azure/templates/fast_tests.yml b/tests/azure/templates/fast_tests.yml index fdb1ea0d130a2ca083e426f33ccf326a7049308b..ac26ce8f0bb19ee5ffd1af63771174ad273e0004 100644 --- a/tests/azure/templates/fast_tests.yml +++ b/tests/azure/templates/fast_tests.yml @@ -1,6 +1,6 @@ --- parameters: - - name: scenario + - name: distro type: string default: fedora-latest - name: build_number @@ -8,6 +8,9 @@ parameters: - name: ansible_version type: string default: "" + - name: target_python + type: string + default: "/usr/bin/python3" jobs: - template: playbook_fast.yml @@ -15,13 +18,14 @@ jobs: group_number: 1 number_of_groups: 1 build_number: ${{ parameters.build_number }} - scenario: ${{ parameters.scenario }} + distro: ${{ parameters.distro }} ansible_version: ${{ parameters.ansible_version }} python_version: '< 3.12' + target_python: ${{ parameters.target_python }} # - template: pytest_tests.yml # parameters: # build_number: ${{ parameters.build_number }} -# scenario: ${{ parameters.scenario }} +# distro: ${{ parameters.distro }} # ansible_version: ${{ parameters.ansible_version }} # python_version: '< 3.12' diff --git a/tests/azure/templates/playbook_fast.yml b/tests/azure/templates/playbook_fast.yml index a01d2c348f5395a8583892df529769d03b63721d..a00a7875dca1ef99ff1d4e8a8fcbbe6e891e3bfd 100644 --- a/tests/azure/templates/playbook_fast.yml +++ b/tests/azure/templates/playbook_fast.yml @@ -6,7 +6,7 @@ parameters: - name: number_of_groups type: number default: 1 - - name: scenario + - name: distro type: string default: fedora-latest - name: ansible_version @@ -17,28 +17,28 @@ parameters: default: 3.x - name: build_number type: string + - name: target_python + type: string + default: "/usr/bin/python3" jobs: - job: Test_Group${{ parameters.group_number }} - displayName: Run playbook tests ${{ parameters.scenario }} (${{ parameters.group_number }}/${{ parameters.number_of_groups }}) + displayName: Run playbook tests ${{ parameters.distro }} (${{ parameters.group_number }}/${{ parameters.number_of_groups }}) timeoutInMinutes: 360 variables: - template: variables.yaml - - template: variables_${{ parameters.scenario }}.yaml + - template: variables_${{ parameters.distro }}.yaml steps: - task: UsePythonVersion@0 inputs: versionSpec: '${{ parameters.python_version }}' - script: | - pip install \ - "molecule-plugins[docker]" \ - "requests<2.29" \ - "ansible${{ parameters.ansible_version }}" + pip install "ansible${{ parameters.ansible_version }}" retryCountOnTaskFailure: 5 - displayName: Install molecule and Ansible + displayName: Install Ansible - - script: ansible-galaxy collection install community.docker ansible.posix + - script: ansible-galaxy collection install containers.podman retryCountOnTaskFailure: 5 displayName: Install Ansible collections @@ -47,43 +47,35 @@ jobs: displayName: Install dependencies - script: | - rm -rf ~/ansible - mkdir -p ~/.ansible/roles ~/.ansible/library ~/.ansible/module_utils - cp -a roles/* ~/.ansible/roles - cp -a plugins/modules/* ~/.ansible/library - cp -a plugins/module_utils/* ~/.ansible/module_utils - molecule create -s ${{ parameters.scenario }} - retryCountOnTaskFailure: 5 - displayName: Setup test container - env: - ANSIBLE_LIBRARY: ./molecule + . utils/set_test_modules + python3 utils/check_test_configuration.py ${{ parameters.distro }} + displayName: Check test configuration - script: | - . utils/set_test_modules - python utils/check_test_configuration.py ${{ parameters.scenario }} - displayName: Check scenario test configuration + utils/setup_test_container.sh \ + -e podman \ + -a \ + -m 4 \ + -n "ipaserver.test.local" \ + -p ${{ parameters.target_python }} \ + -i ${{ parameters.distro }}-server \ + ${{ parameters.distro }}-test + displayName: Setup target container - script: | . utils/set_test_modules - if ! pytest \ - -m "playbook" \ - --verbose \ - --color=yes \ - --suppress-no-test-exit-code \ - --splits=${{ parameters.number_of_groups }} \ - --group=${{ parameters.group_number }} \ - --randomly-seed=$(date "+%Y%m%d") \ - --junit-xml=TEST-results-group-${{ parameters.group_number }}.xml - then - [ $? -eq 5 ] && true || false - fi + pytest -m "playbook" --verbose --color=yes --suppress-no-test-exit-code --junit-xml=TEST-results-pr-check.xml displayName: Run playbook tests env: - IPA_SERVER_HOST: ${{ parameters.scenario }} - RUN_TESTS_IN_DOCKER: true + ANSIBLE_ROLES_PATH: "${PWD}/roles" + ANSIBLE_LIBRARY: "${PWD}/plugins" + ANSIBLE_MODULE_UTILS: "${PWD}/plugins/module_utils" + IPA_SERVER_HOST: ${{ parameters.distro }}-test + RUN_TESTS_IN_DOCKER: podman IPA_DISABLED_MODULES: ${{ variables.ipa_disabled_modules }} IPA_DISABLED_TESTS: ${{ variables.ipa_disabled_tests }} IPA_VERBOSITY: "-vvv" + IPA_PYTHON_PATH: ${{ parameters.target_python }} - task: PublishTestResults@2 inputs: diff --git a/tests/utils.py b/tests/utils.py index 01991aaae61e5455f9e13a8ad509e1391c38fea3..5d8a806cd135518084adf6d9d8ec285bd3363228 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -43,6 +43,10 @@ def get_ssh_password(): return os.getenv("IPA_SSH_PASSWORD") +def get_python_interpreter(): + return os.getenv("IPA_PYTHON_PATH") + + def get_server_host(): return os.getenv("IPA_SERVER_HOST") @@ -97,6 +101,12 @@ def get_inventory_content(): if sshpass: ipa_server_host += " ansible_ssh_pass=%s" % sshpass + python_interpreter = get_python_interpreter() + if python_interpreter: + ipa_server_host += ( + " ansible_python_interpreter=%s" % python_interpreter + ) + lines = [ "[ipaserver]", ipa_server_host, @@ -138,11 +148,33 @@ def write_logs(result, test_name): log_file.write(result.stderr.decode("utf-8")) +def _truncate(lines, charcount, minlines=0): + output = "" + line_count = 1 + for i in range(len(lines) - 1, -1, -1): + if len(output) + len(lines[i]) + 1 <= charcount or \ + line_count < minlines: + output = lines[i] + "\n" + output + line_count += 1 + else: + remaining = charcount - len(output) - 1 - 4 + if remaining > 60: + output = "... " + lines[i][-(remaining):] + "\n" + output + break + return output + + def _run_playbook(playbook): """ Create a inventory using a temporary file and run ansible using it. The logs of the run will be placed in `tests/logs/`. + + In case of failure the tail of the error message will be displayed + as an assertion message. + + The full log of the execution will be available in the directory + `tests/logs/`. """ with tempfile.NamedTemporaryFile() as inventory_file: inventory_file.write(get_inventory_content()) @@ -152,30 +184,45 @@ def _run_playbook(playbook): if verbose is not None: cmd_options.append(verbose) cmd = ["ansible-playbook"] + cmd_options + [playbook] - # pylint: disable=subprocess-run-check process = subprocess.run( - cmd, cwd=SCRIPT_DIR, stdout=subprocess.PIPE, stderr=subprocess.PIPE + cmd, cwd=SCRIPT_DIR, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False ) test_name = get_test_name_from_playbook_path(playbook) write_logs(process, test_name) - return process + msg = "" + if process.returncode != 0: + status_code_msg = "ansible-playbook return code: {0}".format( + process.returncode + ) + _stdout = process.stdout.decode("utf8") + _stderr = process.stderr.decode("utf8") + # Truncate stdout and stderr in the way that it hopefully + # shows all important information. At least 15 lines of stdout + # (Ansible tasks) and remaining from stderr to fill up to + # maxlen size. + maxlen = 2000 + factor = maxlen / (len(_stdout) + len(_stderr)) + stdout = _truncate(_stdout.splitlines(), + int(factor * len(_stdout)), + minlines=15) + stderr = _truncate(_stderr.splitlines(), maxlen - len(stdout)) + + msg = "\n".join( + [ + "", + "-" * 30 + " Captured stdout " + "-" * 30, + stdout, + "-" * 30 + " Captured stderr " + "-" * 30, + stderr + ] + ) + msg += "-" * 30 + " Playbook Return Code " + "-" * 30 + "\n" + msg += status_code_msg -def _truncate(lines, charcount, minlines=0): - output = "" - line_count = 1 - for i in range(len(lines) - 1, -1, -1): - if len(output) + len(lines[i]) + 1 <= charcount or \ - line_count < minlines: - output = lines[i] + "\n" + output - line_count += 1 - else: - remaining = charcount - len(output) - 1 - 4 - if remaining > 60: - output = "... " + lines[i][-(remaining):] + "\n" + output - break - return output + return process, msg def run_playbook(playbook, allow_failures=False): @@ -184,50 +231,10 @@ def run_playbook(playbook, allow_failures=False): Call ansible (using _run_playbook function) and assert the result of the execution. - - In case of failure the tail of the error message will be displayed - as an assertion message. - - The full log of the execution will be available in the directory - `tests/logs/`. """ - result = _run_playbook(playbook) - - if allow_failures: - return result - - status_code_msg = "ansible-playbook return code: {0}".format( - result.returncode - ) - _stdout = result.stdout.decode("utf8") - _stderr = result.stderr.decode("utf8") - # Truncate stdout and stderr in the way that it hopefully - # shows all important information. At least 15 lines of stdout - # (Ansible tasks) and remaining from stderr to fill up to - # maxlen size. - maxlen = 2000 - factor = maxlen / (len(_stdout) + len(_stderr)) - stdout = _truncate(_stdout.splitlines(), - int(factor * len(_stdout)), - minlines=15) - stderr = _truncate(_stderr.splitlines(), maxlen - len(stdout)) - - assert_msg = "\n".join( - [ - "", - "-" * 30 + " Captured stdout " + "-" * 30, - stdout, - "-" * 30 + " Captured stderr " + "-" * 30, - stderr, - "-" * 30 + " Playbook Return Code " + "-" * 30, - status_code_msg, - ] - ) - - # Need to get the last bytes of msg otherwise Azure - # will cut it out. - assert result.returncode == 0, assert_msg[-2500:] - + result, assert_msg = _run_playbook(playbook) + if not allow_failures: + assert result.returncode == 0, assert_msg return result diff --git a/utils/run-tests.sh b/utils/run-tests.sh index e814aed0148d1207ee90599a0eaf374814295792..140998be567eb4a25085a9f044e407d64a5b6f22 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 9f94d2c725ebff31d35ef7e1c683a01dcbace292..daa47dcf55d3b7e3459be44d4c6db94224e27e75 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 0000000000000000000000000000000000000000..0916c27f530fdf706acd63e8ab8f03a0742942e9 --- /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 0000000000000000000000000000000000000000..ca05f820c1a9ceef91d6cae55991595fcb84c6be --- /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 0000000000000000000000000000000000000000..b1c568308840af52e3af7c07a7f39ffb8a33bed5 --- /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