Ansible.sh

#!/usr/bin/env bash
# coding: utf-8
# bash: 3.x
# Copyright © 2025 Florent Claerhout.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


set -euo pipefail


function _abort {
    printf "\033[91m%s -- aborting\033[0m\n" "$*" >&2
    exit 1
}


function create_ansible_venv {
    local -r dir_path=${1:?missing dir_path}

    local -r python_version_major="3.13"

    # install platform dependencies:
    case "${OSTYPE}" in
        darwin*)

            if pkgutil --packages | grep com.apple.pkg.CLTools_Executables
            then
                echo "CLT already installed, skipping"
            else
                xcode-select --install
                echo "Waiting for CLT to be installed"
                while ! xcode-select -p > /dev/null 2>&1
                do
                    sleep 1
                done
            fi

            case $(sw_vers --productVersion) in
                # python 3.9
                26.*)
                    local -r python_version="3.13.9"
                    curl https://www.python.org/ftp/python/${python_version}/python-${python_version}-macos11.pkg --output ~/Downloads/python-${python_version}.pkg
                    open -W ~/Downloads/python-${python_version}.pkg
                    ;;
                *)
                    _abort "Yet unsupported macOS version -- open an issue to request support"
            esac
            ;;
        *)
            _abort "Yet unsupported platform -- open an issue to request support"
    esac

    if which python${python_version_major} >/dev/null 2>&1
    then
        echo "Using $(python${python_version_major} --version)"
    else
        _abort "Python${python_version_major} is missing -- please investigate"
    fi

    if [[ -f "${dir_path}/bin/ansible" ]]
    then
        echo "ansible venv already created, skipping"
    else
        rm -rf "${dir_path}"
        python${python_version_major} -m venv "${dir_path}"
        "${dir_path}/bin/pip" --no-cache-dir install ansible certifi
    fi
}


function print_ansible_dir_path {
    local -r ansible_file_path=$(which ansible)
    if [[ -z ${ansible_file_path} ]]
    then
        local -r ansible_venv_path="/tmp/ansible.venv"
        create_ansible_venv "${ansible_venv_path}" 1>&2
        echo "${ansible_venv_path}/bin"
    else
        dirname "${ansible_file_path}"
    fi
}


function print_usage {
    cat <<-EOF
    Ansible wrapper: install Ansible if needed and run it.

    Usage:
      $(basename "$0") [options] VARIANT ARGV…

    Options:
      -C PATH  reset working directory
      -e       edit source file and exit
      -h       print this help and exit
      -x       trace execution

    Variants:
      adhoc -> ansible
      playbook -> ansible-playbook

    Examples:
      ansible.sh adhoc -m ping localhost
      ansible.sh playbook -i inventory.yml playbook.yml
    EOF
}


while getopts "C:ehx" OPT
do
    case "${OPT}" in
        C)
            cd "${OPTARG}"
            ;;
        e)
            edit -W "${0}"
            exit $?
            ;;
        h)
            print_usage
            exit 0
            ;;
        x)
            set -x
            ;;
        ?)
            _abort "syntax error -- use -h for help"
    esac
done


shift $((OPTIND-1))


if [[ $# -eq 0 ]]
then
    print_usage
else
    ANSIBLE_DIR_PATH=$(print_ansible_dir_path)
    export SSL_CERT_FILE=$("${ANSIBLE_DIR_PATH}/python3" -m certifi)
    case "${1}" in
        adhoc)
            "${ANSIBLE_DIR_PATH}/ansible" "${@:2}"
            ;;
        playbook)
            "${ANSIBLE_DIR_PATH}/ansible-playbook" "${@:2}"
            ;;
        *)
            _abort "${1}: unsupported command -- use -h for help"
    esac
fi