#!/usr/bin/env bash # MIT License # # Copyright (c) 2018 GantSign Ltd. # # 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. # Molecule Wrapper the wrapper script for Molecule # https://github.com/gantsign/molecule-wrapper set -e WRAPPER_VERSION=0.9.12 VERSION_DIR='.moleculew' PYTHON_VERSION_FILE="$VERSION_DIR/python_version" ANSIBLE_VERSION_FILE="$VERSION_DIR/ansible_version" MOLECULE_VERSION_FILE="$VERSION_DIR/molecule_version" BUILD_DEPENDENCIES_INSTALLLED=false PYENV_INSTALLED=false ANSIBLE_VERSION='' MOLECULE_VERSION='' PYTHON_VERSION='' USE_SYSTEM_DEPENDENCIES=false PRE_ARGS=() MOLECULE_CMD='' POST_ARGS=() export PATH="$HOME/.pyenv/bin:$HOME/.local/bin:$PATH" hr() { for ((i = 1; i <= 80; i++)); do printf '*' done echo '' } banner() { hr echo "$1" hr } run_as_root() { if [[ $EUID -eq 0 ]]; then "$@" elif [ -x "$(command -v sudo)" ]; then sudo "$@" else echo "Error: sudo is not installed" >&2 exit 1 fi } build_dependencies_present() { if [[ $BUILD_DEPENDENCIES_INSTALLLED == true ]]; then return fi if [[ $USE_SYSTEM_DEPENDENCIES == true ]]; then return fi # https://github.com/pyenv/pyenv/wiki/common-build-problems if [[ -x "$(command -v apt-get)" ]]; then banner 'Installing build dependencies' run_as_root apt-get update run_as_root apt-get install --assume-yes \ make build-essential libssl-dev zlib1g-dev libbz2-dev \ libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev \ libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev \ git jq echo '' elif [[ -x "$(command -v dnf)" ]]; then banner 'Installing build dependencies' run_as_root dnf install \ zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \ openssl-devel xz xz-devel libffi-devel \ git curl jq echo '' elif [[ -x "$(command -v yum)" ]]; then banner 'Installing build dependencies' run_as_root yum install \ zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \ openssl-devel xz xz-devel libffi-devel \ git curl jq echo '' elif [[ -x "$(command -v zypper)" ]]; then banner 'Installing build dependencies' run_as_root zypper install \ zlib-devel bzip2 libbz2-devel readline-devel sqlite3 sqlite3-devel \ libopenssl-devel xz xz-devel \ git curl jq echo '' fi BUILD_DEPENDENCIES_INSTALLLED=true } pyenv_present() { if [[ $PYENV_INSTALLED == true ]]; then return fi if [[ $USE_SYSTEM_DEPENDENCIES == true ]]; then return fi if [[ -x "$(command -v pyenv)" ]]; then PYENV_INSTALLED=true return fi build_dependencies_present banner "Installing pyenv for user $USER" bash <(curl --location https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer) echo '' PYENV_INSTALLED=true } query_latest_python_version2() { pyenv_present PYTHON_VERSION="$(~/.pyenv/plugins/python-build/bin/python-build --definitions | grep --color=never '^2\.' | grep --invert-match '\-dev$' | tail -1)" } query_latest_python_version3() { pyenv_present PYTHON_VERSION="$(~/.pyenv/plugins/python-build/bin/python-build --definitions | grep --color=never '^3\.' | grep --invert-match '\-dev$' | tail -1)" } query_latest_package_version() { if [[ ! -x "$(command -v curl)" ]]; then build_dependencies_present fi if [[ ! -x "$(command -v jq)" ]]; then build_dependencies_present fi if [[ ! -x "$(command -v curl)" ]]; then echo 'Error: curl is not installed.' >&2 exit 1 fi if [[ ! -x "$(command -v jq)" ]]; then echo 'Error: jq is not installed.' >&2 exit 1 fi local version # shellcheck disable=SC2034 version=$(curl --fail --silent --show-error --location "https://pypi.org/pypi/$2/json" | jq --raw-output '.info.version') eval "$1=\"\$version\"" } docker_present() { if [[ -x "$(command -v docker)" ]]; then return fi if [[ $USE_SYSTEM_DEPENDENCIES == true ]]; then echo 'Error: docker is not installed.' >&2 exit 1 fi build_dependencies_present banner 'Installing Docker' sh <(curl --fail --silent --show-error --location https://get.docker.com) run_as_root usermod --append --groups docker "$USER" banner "User '$USER' has been added to the 'docker' group. Logout/restart and log back in for changes to take effect." exit } python_present() { if [[ $PYTHON_VERSION == system ]]; then if [[ ! -x "$(command -v python)" ]]; then echo 'Error: python is not installed.' >&2 exit 1 fi if [[ ! -x "$(command -v pip)" ]]; then echo 'Error: pip is not installed.' >&2 exit 1 fi PYTHON_EXE="$(command -v python)" else if [[ ! -x "$(command -v git)" ]]; then echo 'Error: git is not installed.' >&2 exit 1 fi pyenv_present export PYENV_VERSION="$PYTHON_VERSION" if [[ ! -d "$HOME/.pyenv/versions/$PYTHON_VERSION" ]]; then build_dependencies_present banner "Making Python version $PYTHON_VERSION available using pyenv" pyenv install "$PYTHON_VERSION" echo '' fi eval "$(pyenv init -)" PYTHON_EXE="$(pyenv which python)" fi } virtualenv_presant() { if [[ ! -x "$(command -v virtualenv)" ]]; then banner "Installing virtualenv for user $USER" pip install --user virtualenv echo '' fi } install_ansible() { banner "Installing Ansible $ANSIBLE_VERSION into virtualenv $VIRTUAL_ENV" pip install "ansible==$ANSIBLE_VERSION" echo '' } install_molecule() { banner "Installing Molecule $MOLECULE_VERSION into virtualenv $VIRTUAL_ENV" # Workaround https://github.com/ansible-community/molecule/issues/2676 pip install 'sh==1.12.14' pip install "molecule[docker]==$MOLECULE_VERSION" echo '' } wrapper_clean() { local MOLECULE_WRAPPER_HOME="$HOME/.moleculew" read -r -p "Delete ${MOLECULE_WRAPPER_HOME} (y/n)? " yn case $yn in [Yy]|YES|yes|Yes) rm -rf "$MOLECULE_WRAPPER_HOME"; exit ;; *) exit ;; esac } wrapper_upgrade() { curl --fail --silent --show-error --location --output moleculew.new \ 'https://raw.githubusercontent.com/gantsign/molecule-wrapper/master/moleculew' \ && chmod 'u+x' moleculew.new \ && mv --force moleculew.new moleculew local NEW_VERSION NEW_VERSION="$(./moleculew wrapper-version)" if [ "$WRAPPER_VERSION" != "$NEW_VERSION" ]; then echo "Upgraded wrapper from version $WRAPPER_VERSION to $NEW_VERSION" else echo "You are already using the latest version" fi exit } wrapper_version() { echo "$WRAPPER_VERSION" exit } print_versions() { echo "Python: $PYTHON_VERSION" echo "Ansible: $ANSIBLE_VERSION" echo "Molecule: $MOLECULE_VERSION" } wrapper_versions() { detemine_versions print_versions exit } wrapper_freeze() { detemine_versions banner 'Freezing versions' mkdir -p "$VERSION_DIR" echo "$PYTHON_VERSION" > "$PYTHON_VERSION_FILE" echo "$ANSIBLE_VERSION" > "$ANSIBLE_VERSION_FILE" echo "$MOLECULE_VERSION" > "$MOLECULE_VERSION_FILE" print_versions exit } wrapper_unfreeze() { banner 'Un-freezing versions' if [[ -f "$PYTHON_VERSION_FILE" ]]; then rm --verbose "$PYTHON_VERSION_FILE" fi if [[ -f "$ANSIBLE_VERSION_FILE" ]]; then rm --verbose "$ANSIBLE_VERSION_FILE" fi if [[ -f "$MOLECULE_VERSION_FILE" ]]; then rm --verbose "$MOLECULE_VERSION_FILE" fi exit } wrapper_upgrade_versions() { detemine_versions banner 'Upgrading versions' local CURRENT_PYTHON_VERSION="$PYTHON_VERSION" local CURRENT_ANSIBLE_VERSION="$ANSIBLE_VERSION" local CURRENT_MOLECULE_VERSION="$MOLECULE_VERSION" query_latest_python_version2 query_latest_package_version ANSIBLE_VERSION ansible query_latest_package_version MOLECULE_VERSION molecule echo '' echo 'New versions:' if [[ "$CURRENT_PYTHON_VERSION" == "$PYTHON_VERSION" ]]; then echo "Python: $CURRENT_PYTHON_VERSION (no change)" else echo "Python: $CURRENT_PYTHON_VERSION -> $PYTHON_VERSION" fi if [[ "$CURRENT_ANSIBLE_VERSION" == "$ANSIBLE_VERSION" ]]; then echo "Ansible: $CURRENT_ANSIBLE_VERSION (no change)" else echo "Ansible: $CURRENT_ANSIBLE_VERSION -> $ANSIBLE_VERSION" fi if [[ "$CURRENT_MOLECULE_VERSION" == "$MOLECULE_VERSION" ]]; then echo "Molecule: $CURRENT_MOLECULE_VERSION (no change)" else echo "Molecule: $CURRENT_MOLECULE_VERSION -> $MOLECULE_VERSION" fi echo '' wrapper_freeze } wrapper_help() { activate_virtualenv molecule --help echo " Molecule Wrapper Additional options: --ansible VERSION Use the specified version of Ansible --molecule VERSION Use the specified version of Molecule --python VERSION Use the specified version of Python --use-system-dependencies Use system dependencies Additional commands: wrapper-clean Removes all the wrapper virtual environments wrapper-freeze Freezes the dependency versions being used wrapper-unfreeze Un-freezes the dependency versions wrapper-upgrade Upgrades the Molecule Wrapper to the latest version wrapper-upgrade-versions Upgrades any frozen dependency versions wrapper-version Displays the current version of Molecule Wrapper " } query_package_versions() { local package_name="$1" local min_version="$2" if [[ ! -x "$(command -v curl)" ]]; then build_dependencies_present > /dev/null fi if [[ ! -x "$(command -v jq)" ]]; then build_dependencies_present > /dev/null fi if [[ ! -x "$(command -v curl)" ]]; then echo 'Error: curl is not installed.' >&2 exit 1 fi if [[ ! -x "$(command -v jq)" ]]; then echo 'Error: jq is not installed.' >&2 exit 1 fi if [[ ! -x "$(command -v sort)" ]]; then echo 'Error: sort is not installed.' >&2 exit 1 fi for i in $(curl --fail --silent --show-error \ --location "https://pypi.org/pypi/$package_name/json" \ | jq --raw-output ".releases | keys | .[], \"$min_version.\"" \ | grep --invert-match '[a-zA-Z]' \ | sort --version-sort --reverse) ; do if [[ "$i" == "$min_version." ]]; then break fi echo "$i" done } wrapper_options_ansible() { echo 'latest' query_package_versions 'ansible' '2.7' } wrapper_options_molecule() { echo 'latest' query_package_versions 'molecule' '2.20' } wrapper_options_python() { if [[ ! -x "$(command -v sort)" ]]; then echo 'Error: sort is not installed.' >&2 exit 1 fi pyenv_present > /dev/null local min_version='2.7' echo 'latest' for i in $( (echo "$min_version." && \ ~/.pyenv/plugins/python-build/bin/python-build --definitions) \ | grep --color=never '^[0-9]' \ | grep --invert-match '\-dev$' \ | sort --version-sort --reverse) ; do if [[ "$i" == "$min_version." ]]; then break fi echo "$i" done } wrapper_options_scenario() { if [ -f 'moleculew' ]; then activate_virtualenv > /dev/null fi python << EOF import os import sys import six import yaml molecule_dir = 'molecule' if not os.path.isdir(molecule_dir): sys.exit() scenarios = [] default = False for filename in os.listdir(molecule_dir): scenario_dir = os.path.join(molecule_dir, filename) if not os.path.isdir(scenario_dir): continue molecule_yaml = os.path.join(scenario_dir, 'molecule.yml') if not os.path.isfile(molecule_yaml): continue with open(molecule_yaml, 'r') as stream: try: contents = yaml.safe_load(stream) except yaml.YAMLError as exc: continue if not isinstance(contents, dict): continue scenario = contents.get('scenario') if scenario is None: continue if not isinstance(scenario, dict): continue name = scenario.get('name') if name is None: continue if not isinstance(name, six.string_types): continue if name == 'default': default = True else: scenarios.append(name) scenarios.sort() if default: scenarios.append('default') for scenario in scenarios: print(scenario) EOF } wrapper_virtualenv() { activate_virtualenv > /dev/null echo "$VIRTUAL_ENV" } parse_args() { set +e while [[ $# -gt 0 ]]; do key="$1" case $key in --python=*) PYTHON_VERSION="${1#*=}" shift ;; --python) shift PYTHON_VERSION="$1" shift ;; --ansible=*) ANSIBLE_VERSION="${1#*=}" shift ;; --ansible) shift ANSIBLE_VERSION="$1" shift ;; --molecule=*) MOLECULE_VERSION="${1#*=}" shift ;; --molecule) shift MOLECULE_VERSION="$1" shift ;; --use-system-dependencies) USE_SYSTEM_DEPENDENCIES=true shift ;; --help) MOLECULE_CMD='wrapper-help' break ;; wrapper-*) MOLECULE_CMD="$1" shift ;; check|converge|create|dependency|destroy|idempotence|init|lint|list|login|matrix|prepare|side-effect|syntax|test|verify) if [[ "$MOLECULE_CMD" != '' ]]; then shift else MOLECULE_CMD="$1" shift for arg in "$@"; do POST_ARGS+=("$arg") done break fi ;; *) PRE_ARGS+=("$1") shift ;; esac done set -e } detemine_versions() { if [[ $USE_SYSTEM_DEPENDENCIES == false ]]; then USE_SYSTEM_DEPENDENCIES="$MOLECULEW_USE_SYSTEM" fi if [[ $PYTHON_VERSION == '' ]]; then PYTHON_VERSION="$MOLECULEW_PYTHON" fi if [[ $ANSIBLE_VERSION == '' ]]; then ANSIBLE_VERSION="$MOLECULEW_ANSIBLE" fi if [[ $MOLECULE_VERSION == '' ]]; then MOLECULE_VERSION="$MOLECULEW_MOLECULE" fi if [[ $USE_SYSTEM_DEPENDENCIES == true ]]; then if [[ $PYTHON_VERSION != '' ]]; then echo "Error: --python and --use-system-dependencies cannot be used together" >&2 exit 1 fi PYTHON_VERSION=system elif [[ $PYTHON_VERSION == '' ]] || [[ $PYTHON_VERSION == 'default' ]]; then if [[ -f $PYTHON_VERSION_FILE ]]; then PYTHON_VERSION=$(<"$PYTHON_VERSION_FILE") fi if [[ $PYTHON_VERSION == '' ]]; then query_latest_python_version2 fi elif [[ $PYTHON_VERSION == 'latest' ]] || [[ $PYTHON_VERSION == 'latest2' ]]; then query_latest_python_version2 elif [[ $PYTHON_VERSION == 'latest3' ]]; then query_latest_python_version3 fi if [[ $ANSIBLE_VERSION == '' ]] || [[ $ANSIBLE_VERSION == 'default' ]]; then if [[ -f $ANSIBLE_VERSION_FILE ]]; then ANSIBLE_VERSION=$(<"$ANSIBLE_VERSION_FILE") fi if [[ $ANSIBLE_VERSION == '' ]]; then query_latest_package_version ANSIBLE_VERSION ansible fi elif [[ $ANSIBLE_VERSION == 'latest' ]]; then query_latest_package_version ANSIBLE_VERSION ansible fi if [[ $MOLECULE_VERSION == '' ]] || [[ $MOLECULE_VERSION == 'default' ]]; then if [[ -f $MOLECULE_VERSION_FILE ]]; then MOLECULE_VERSION=$(<$MOLECULE_VERSION_FILE) fi if [[ $MOLECULE_VERSION == '' ]]; then query_latest_package_version MOLECULE_VERSION molecule fi elif [[ $MOLECULE_VERSION == 'latest' ]]; then query_latest_package_version MOLECULE_VERSION molecule fi } activate_virtualenv() { detemine_versions MOLECULE_WRAPPER_ENV="$HOME/.moleculew/molecule/$MOLECULE_VERSION/ansible/$ANSIBLE_VERSION/python/$PYTHON_VERSION" if [ ! -f "$MOLECULE_WRAPPER_ENV/bin/activate" ]; then build_dependencies_present docker_present python_present virtualenv_presant banner "Initializing virtualenv $MOLECULE_WRAPPER_ENV" virtualenv "--python=$PYTHON_EXE" "$MOLECULE_WRAPPER_ENV" # shellcheck disable=SC1090 source "$MOLECULE_WRAPPER_ENV/bin/activate" echo '' install_ansible install_molecule else # shellcheck disable=SC1090 source "$MOLECULE_WRAPPER_ENV/bin/activate" fi } parse_args "$@" case $MOLECULE_CMD in wrapper-clean) wrapper_clean ;; wrapper-freeze) wrapper_freeze ;; wrapper-help) wrapper_help ;; wrapper-install) activate_virtualenv ;; wrapper-options-ansible) wrapper_options_ansible ;; wrapper-options-molecule) wrapper_options_molecule ;; wrapper-options-python) wrapper_options_python ;; wrapper-options-scenario) wrapper_options_scenario ;; wrapper-unfreeze) wrapper_unfreeze ;; wrapper-upgrade) wrapper_upgrade ;; wrapper-upgrade-versions) wrapper_upgrade_versions ;; wrapper-version) wrapper_version ;; wrapper-versions) wrapper_versions ;; wrapper-virtualenv) wrapper_virtualenv ;; wrapper-*) echo "Unsupported command: $1" >&2 exit 1 ;; *) activate_virtualenv # shellcheck disable=SC2086 exec molecule "${PRE_ARGS[@]}" $MOLECULE_CMD "${POST_ARGS[@]}" ;; esac