├── bin ├── atb-provision.sh ├── atb-get-facts.sh ├── atb-list-variables.sh ├── atb-export-vm.sh ├── atb-init.sh ├── atb-role-deps.sh └── atb-init-role.sh ├── README.md └── install.sh /bin/atb-provision.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | # Run Ansible manually on a host managed by Vagrant. Fix the path to the Vagrant 6 | # private key before use! 7 | 8 | set -o errexit # abort on nonzero exitstatus 9 | set -o nounset # abort on unbound variable 10 | 11 | #{{{ Variables 12 | inventory_file=".vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory" 13 | ssh_user=vagrant 14 | private_key_path="${HOME}/.vagrant.d/insecure_private_key" 15 | #}}} 16 | #{{{ Functions 17 | 18 | usage() { 19 | cat << _EOF_ 20 | Usage: ${0} [PLAYBOOK] [ARGS] 21 | Runs Ansible-playbook manually on a host controlled by Vagrant. Run this script 22 | from the same directory as the Vagrantfile. 23 | 24 | PLAYBOOK the playbook to be run (default: ansible/site.yml) 25 | ARGS other options that are passed on to ‘ansible-playbook’ verbatim 26 | _EOF_ 27 | } 28 | 29 | find_private_key() { 30 | local vagrant_key 31 | local num_keys_found 32 | 33 | vagrant_key=$(ls .vagrant/machines/*/virtualbox/private_key) 34 | num_keys_found=$(echo "${vagrant_key}" | wc --lines) 35 | 36 | if [ "${num_keys_found}" -gt "1" ]; then 37 | cat >&2 << _EOF_ 38 | I found multiple private keys in the current Vagrant environment. Sorry, 39 | I can't handle that (yet?). 40 | _EOF_ 41 | exit 2 42 | fi 43 | 44 | private_key_path="${vagrant_key}" 45 | } 46 | 47 | #}}} 48 | # {{{ Command line parsing 49 | 50 | if [ "${#}" -gt "0" -a -f "${1}" ]; then 51 | playbook="${1}" 52 | shift 53 | else 54 | playbook="ansible/site.yml" 55 | fi 56 | 57 | 58 | # }}} 59 | # Script proper 60 | 61 | # Ignore SSH host key checking (it would create an entry in ~/.ssh/known_hosts 62 | # and fail when you create a new VM. 63 | export ANSIBLE_HOST_KEY_CHECKING=False 64 | 65 | ansible-playbook \ 66 | "${playbook}" \ 67 | --inventory="${inventory_file}" \ 68 | --connection=ssh \ 69 | --user="${ssh_user}" \ 70 | --private-key="${private_key_path}" \ 71 | "$@" 72 | 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible toolbox 2 | 3 | A collection of scripts to be used in conjunction with [`ansible-skeleton`](https://github.com/bertvv/ansible-skeleton) and [`ansible-role-skeleton`](https://github.com/bertvv/ansible-role-skeleton). 4 | 5 | ## Installation 6 | 7 | 1. Eiter [download the source](https://github.com/bertvv/ansible-toolbox/archive/master.zip) and extract it somewhere, or clone the repository 8 | 9 | `git clone https://github.com/bertvv/ansible-toolbox.git` 10 | 11 | 2. Run the installation script: `./install.sh ` 12 | 13 | The installer will copy all scripts into the specified directory. If you omit the directory, it will copy the scripts into `/usr/local/bin` when invoked by the superuser, or into `~/.local/bin` when run by a normal user. The installation directory must exist. 14 | 15 | ## The scripts 16 | 17 | - `atb-export-vm`: export a VirtualBox VM to an .ova file, removing the shared folder created by Vagrant. 18 | - `atb-get-facts`: list all [facts](https://docs.ansible.com/ansible/playbooks_variables.html#information-discovered-from-systems-facts) (`ansible_*` variables) from a Vagrant box. 19 | - `atb-init`: set up scaffolding code for a Vagrant+Ansible development environment based on . 20 | - `atb-init-role`: set up scaffolding code for an Ansible role based on . 21 | - `atb-list-variables`: search the current directory (assumed to contain the code for an Ansible role) for role variables, and prints an alphabetical list. The list can also be formatted as a Markdown table, useful for role documentation. 22 | - `atb-provision`: Run `ansible-playbook` on a host managed by Vagrant. The benefit of this (compared to `vagrant provision`) is that you can limit execution to specific hosts (`--limit=`) or tags (`--tags=`), or that you can pass arbitrary options to `ansible-playbook`. 23 | - `atb-role-deps.sh`: Installs all roles mentioned in `ansible/site.yml` from Ansible Galaxy or Github. 24 | 25 | Remark that none of the scripts require superuser privileges. Call any of the scripts with option `-h` or `--help` for specific documentation. 26 | 27 | ## Contributing 28 | 29 | Issues, feature requests, ideas are appreciated and can be posted in the Issues section. Pull requests are also very welcome. Preferably, create a topic branch and when submitting, squash your commits into one (with a descriptive message). 30 | 31 | ## License 32 | 33 | BSD 34 | 35 | ## Author Information 36 | 37 | Bert Van Vreckem (bert.vanvreckem@gmail.com) 38 | 39 | -------------------------------------------------------------------------------- /bin/atb-get-facts.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | # Retrieves Ansible variables (facts, info about the host) from the 6 | # specified host (assumed to be a Vagrant VM). 7 | # 8 | # See usage() for details. 9 | 10 | #{{{ Bash settings 11 | # abort on nonzero exitstatus 12 | set -o errexit 13 | # abort on unbound variable 14 | set -o nounset 15 | # don't hide errors within pipes 16 | set -o pipefail 17 | #}}} 18 | #{{{ Variables 19 | readonly SCRIPT_NAME=$(basename "${0}") 20 | readonly SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 21 | IFS=$'\t\n' # Split on newlines and tabs (but not on spaces) 22 | 23 | # Colours 24 | readonly YELLOW='\e[0;33m' 25 | readonly BLUE='\e[0;34m' 26 | readonly RESET='\e[0m' 27 | 28 | # Inventory file 29 | readonly INVENTORY="${PWD}/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory" 30 | #}}} 31 | 32 | main() { 33 | check_if_help_is_wanted "${@}" 34 | check_if_inventory_exists 35 | 36 | hosts=$(determine_target_hosts "${@}") 37 | 38 | for host in ${hosts}; do 39 | get_facts "${host}" 40 | done 41 | } 42 | 43 | #{{{ Helper functions 44 | 45 | check_if_help_is_wanted() { 46 | if [ "${#}" -gt "0" ]; then 47 | if [ "${1}" = '-h' -o "${1}" = '--help' -o "${1}" = '-?' ]; then 48 | usage 49 | exit 50 | fi 51 | fi 52 | } 53 | 54 | check_if_inventory_exists() { 55 | if [ ! -f "${INVENTORY}" ]; then 56 | cat >&2 << _EOF_ 57 | Vagrant inventory file not found. Execute this command from a directory 58 | containing a Vagrantfile. 59 | _EOF_ 60 | usage 61 | exit 1 62 | fi 63 | } 64 | 65 | determine_target_hosts() { 66 | if [ "${#}" -ne "0" ]; then 67 | echo "${@}" 68 | else 69 | enumerate_vagrant_hosts 70 | fi 71 | } 72 | 73 | enumerate_vagrant_hosts() { 74 | vagrant status \ 75 | | tail -n +3 \ 76 | | head -n -4 \ 77 | | cut -d' ' -f1 78 | } 79 | 80 | # Print usage message on stdout 81 | usage() { 82 | cat << _EOF_ 83 | Usage: ${SCRIPT_NAME} [OPTION] [HOST...] 84 | 85 | Retrieves Ansible variables/facts from the specified hosts (assumed to be 86 | Vagrant VMs. If no host was specified, all hosts of the current Vagrant 87 | environment are selected. 88 | 89 | OPTIONS: 90 | 91 | -?, -h, --help Prints this help message and exits (with status 0) 92 | 93 | EXAMPLES: 94 | 95 | ${SCRIPT_NAME} box1 box2 96 | _EOF_ 97 | } 98 | 99 | # Usage: get_facts HOST 100 | get_facts() { 101 | local host="${1}" 102 | echo -e "${YELLOW}Ansible variables/facts for host ${BLUE}${host}${RESET}" 103 | 104 | ansible "${host}" \ 105 | --module-name=setup \ 106 | --inventory-file="${INVENTORY}" 107 | } 108 | 109 | #}}} 110 | 111 | main "${@}" 112 | 113 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | # Installation script for https://github.com/bertvv/ansible-toolbox 6 | 7 | set -o errexit # abort on nonzero exitstatus 8 | set -o nounset # abort on unbound variable 9 | 10 | #{{{ Variables 11 | 12 | # Color definitions 13 | readonly reset='\e[0m' 14 | readonly cyan='\e[0;36m' 15 | readonly red='\e[0;31m' 16 | readonly yellow='\e[0;33m' 17 | 18 | # Default installation directory 19 | if [ "${UID}" -eq "0" ]; then 20 | install_dir="/usr/local/bin" 21 | else 22 | install_dir="${HOME}/.local/bin" 23 | fi 24 | 25 | src_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/bin" 26 | #}}} 27 | #{{{ Functions 28 | 29 | usage() { 30 | cat << _EOF_ 31 | Usage: ${0} [OPTIONS] [INSTALL_DIR] 32 | 33 | Installs the scripts into INSTALL_DIR. If the installation 34 | directory was not specified, install into /usr/local/bin 35 | when invoked by the superuser, or else into ~/.local/bin. 36 | 37 | Options: 38 | 39 | -h, --help Print this message and exit 40 | _EOF_ 41 | } 42 | 43 | # Usage: validate_install_dir DIR 44 | # Checks whether the installation directory exists 45 | validate_install_dir() { 46 | local dir="${1}" 47 | debug "Checking whether installation dir ${dir} exists" 48 | 49 | if [ ! -d "${dir}" ]; then 50 | error "Installation directory ‘${dir}’ doesn't exist" 51 | error "Create it or specify an existing directory" 52 | usage 53 | exit 1 54 | fi 55 | } 56 | 57 | # Usage install_script PATH 58 | # Installs the script, specified by its full pathname to the 59 | # installation directory. 60 | install_script() { 61 | local script_path="${1}" 62 | 63 | # strip path and extension 64 | local script_name="${script_path##*/}" 65 | local cmd_name="${script_name%.sh}" 66 | 67 | #debug "Installing ${script_name} to ${cmd_name}" 68 | 69 | install --compare --verbose \ 70 | "${script_path}" "${install_dir}/${cmd_name}" 71 | } 72 | 73 | # Usage: update_path_in_bashrc 74 | # Adds the installation directory to the end of ~/.bashrc 75 | update_path_in_bashrc() { 76 | info "Adding installation directory to ~/.bashrc" 77 | cat >> "${HOME}/.bashrc" <<-_EOF_ 78 | 79 | # Add Ansible Toolbox scripts to the PATH 80 | export PATH=\${PATH}:${install_dir} 81 | _EOF_ 82 | } 83 | 84 | # Usage ensure_scripts_on_PATH 85 | # Check whether the installed scripts are on the ${PATH} and if not, add 86 | # installation directory to ~/.bashrc if possible 87 | ensure_scripts_on_path() { 88 | 89 | if ! which atb-init > /dev/null 2>&1; then 90 | if [ -f "${HOME}/.bashrc" ]; then 91 | update_path_in_bashrc 92 | else 93 | info "Warning: the installation directory is not in the PATH" 94 | fi 95 | fi 96 | } 97 | 98 | # Usage: info [ARG]... 99 | # 100 | # Prints all arguments on the standard output stream 101 | info() { 102 | printf "${yellow}>>> %s${reset}\n" "${*}" 103 | } 104 | 105 | # Usage: debug [ARG]... 106 | # 107 | # Prints all arguments on the standard output stream 108 | debug() { 109 | printf "${cyan}### %s${reset}\n" "${*}" 110 | } 111 | 112 | # Usage: error [ARG]... 113 | # 114 | # Prints all arguments on the standard error stream 115 | error() { 116 | printf "${red}!!! %s${reset}\n" "${*}" 1>&2 117 | } 118 | 119 | #}}} 120 | #{{{ Command line parsing 121 | 122 | if [ "$#" -gt "0" ]; then 123 | if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then 124 | usage 125 | exit 0 126 | else 127 | install_dir="$1" 128 | fi 129 | fi 130 | 131 | #}}} 132 | # Script proper 133 | 134 | validate_install_dir "${install_dir}" 135 | 136 | debug "Installing scripts: ${src_dir}/*.sh" 137 | for script in ${src_dir}/*.sh; do 138 | install_script "${script}" 139 | done 140 | 141 | ensure_scripts_on_path 142 | -------------------------------------------------------------------------------- /bin/atb-list-variables.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | #/ Usage: atb-list-variables [-t|--table] [ROLENAME] 6 | #/ atb-list-variables [-h|--help] 7 | #/ 8 | #/ Searches the current directory (assumed to contain an Ansible role) for 9 | #/ role variables. This assumes each variable is prefixed with the role 10 | #/ name, e.g. bind_service. 11 | #/ 12 | #/ If no ROLENAME was specified explicitly, the name of the current directory 13 | #/ is used. 14 | #/ 15 | #/ OPTIONS 16 | #/ -h, --help 17 | #/ Print this help message 18 | #/ -t, --table 19 | #/ Print the variables as a Markdown table, suitable for 20 | #/ role documentation. 21 | #/ 22 | #/ EXAMPLES 23 | #/ atb-list-variables bind 24 | #/ atb-list-variables --table vsftpd 25 | # 26 | # Dependencies: 27 | # 28 | # - coreutils 29 | # - grep 30 | # - sed 31 | 32 | #{{{ Bash settings 33 | # abort on nonzero exitstatus 34 | set -o errexit 35 | # abort on unbound variable 36 | set -o nounset 37 | # don't hide errors within pipes 38 | set -o pipefail 39 | #}}} 40 | #{{{ Variables 41 | readonly script_name=$(basename "${0}") 42 | readonly script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 43 | IFS=$'\t\n' # Split on newlines and tabs (but not on spaces) 44 | 45 | # Color definitions 46 | readonly reset='\e[0m' 47 | readonly cyan='\e[0;36m' 48 | readonly red='\e[0;31m' 49 | readonly yellow='\e[0;33m' 50 | # Debug info ('on' to enable) 51 | readonly debug='on' 52 | 53 | # Script configuration, default values 54 | formatter='cat' # How should the output be formatted 55 | prefix="${PWD##*/}" # Role variable prefix (default = current dir) 56 | #}}} 57 | 58 | main() { 59 | check_args "${@}" 60 | 61 | log "Searching for role variables starting with ${prefix}_" 62 | list_role_variables "${prefix}" \ 63 | | ${formatter} 64 | 65 | log "Variables in vars/ that cannot be set by the user:" 66 | list_role_variables "${prefix}" vars/*.yml 67 | } 68 | 69 | #{{{ Helper functions 70 | 71 | # Usage: list_role_variables ROLE [FILE]... 72 | # 73 | # List all role variables with prefix ROLE_ in YAML or Jinja files within the 74 | # current directory. 75 | list_role_variables() { 76 | local prefix="${1}" 77 | shift 78 | local files_to_search=${*:-$(ls ./*/*.yml ./*/*.j2)} 79 | 80 | grep --no-filename --color=never --only-matching \ 81 | "\b${prefix}_.[a-z0-9_]*\b" ${files_to_search} \ 82 | | sort \ 83 | | sed '/^$/d' \ 84 | | uniq 85 | } 86 | 87 | # Usage: ... | print_markdown_table 88 | # 89 | # Prints a Markdown-formatted table useful for Ansible role documentation 90 | # with text from stdin (variable names) in the first column 91 | print_markdown_table() { 92 | printf '| Variable | Default | Comment |\n' 93 | printf '| :--- | :--- | :--- |\n' 94 | while read -r line 95 | do 96 | printf '| `%s` | | |\n' "${line}" 97 | done 98 | } 99 | 100 | # Print usage message on stdout by parsing start of script comments 101 | usage() { 102 | grep '^#/' "${script_dir}/${script_name}" | sed 's/^#\/\w*//' 103 | } 104 | 105 | # Process command line options 106 | check_args() { 107 | while [ "$#" -gt '0' ]; do 108 | case ${1} in 109 | -h|--help) 110 | usage 111 | exit 0 112 | ;; 113 | -t|--table) 114 | formatter=print_markdown_table 115 | shift 116 | ;; 117 | -*) 118 | error "Unrecognized option: ${1}" 119 | usage 120 | exit 2 121 | ;; 122 | *) 123 | prefix="${1}" 124 | shift 125 | ;; 126 | esac 127 | done 128 | } 129 | 130 | # Usage: log [ARG]... 131 | # 132 | # Prints all arguments on the standard error stream 133 | log() { 134 | printf "${yellow}>>> %s${reset}\\n" "${*}" >&2 135 | } 136 | 137 | # Usage: debug [ARG]... 138 | # 139 | # Prints all arguments on the standard error stream, 140 | # if debug output is enabled 141 | debug() { 142 | [ "${debug}" != 'on' ] || printf "${cyan}### %s${reset}\\n" "${*}" >&2 143 | } 144 | 145 | # Usage: error [ARG]... 146 | # 147 | # Prints all arguments on the standard error stream 148 | error() { 149 | printf "${red}!!! %s${reset}\\n" "${*}" >&2 150 | } 151 | 152 | 153 | #}}} 154 | 155 | main "${@}" 156 | 157 | -------------------------------------------------------------------------------- /bin/atb-export-vm.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | #/ Usage: atb-export-vm VM_NAME 6 | #/ 7 | #/ with VM_NAME the name of a VirtualBox VM. 8 | #/ 9 | #/ Exports a VirtualBox VM to an OVA file, removing the "/vagrant" shared 10 | #/ folder, if it exists. 11 | 12 | #{{{ Bash settings 13 | # abort on nonzero exitstatus 14 | set -o errexit 15 | # abort on unbound variable 16 | set -o nounset 17 | #}}} 18 | 19 | #{{{ Variables 20 | readonly script_name=$(basename "${0}") 21 | readonly script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 22 | IFS=$'\t\n' # Split on newlines and tabs (but not on spaces) 23 | 24 | # Color definitions 25 | readonly reset='\e[0m' 26 | readonly cyan='\e[0;36m' 27 | readonly red='\e[0;31m' 28 | readonly yellow='\e[0;33m' 29 | 30 | readonly today=$(date --iso-8601) 31 | #}}} 32 | 33 | main() { 34 | check_args "${@}" 35 | 36 | local vm="${1}" 37 | 38 | if ! is_a_vm "${vm}"; then 39 | error "${vm} is not a VM." 40 | list_available_vms 41 | exit 1 42 | fi 43 | 44 | if is_vm_running "${vm}"; then 45 | error "${vm} is still running, shut it down first!" 46 | exit 1 47 | fi 48 | 49 | delete_shared_folders "${vm}" 50 | 51 | readonly local ova=$(unique_ova_name "${vm}") 52 | info "Exporting to file: ${ova}" 53 | vboxmanage export "${vm}" --output "${ova}" --options manifest 54 | } 55 | 56 | 57 | #{{{ Helper functions 58 | 59 | # Check if command line arguments are valid 60 | check_args() { 61 | if [ "${#}" -ne "1" ]; then 62 | error "Expected 1 argument, got ${#}" 63 | usage 64 | exit 2 65 | fi 66 | if [ "$1" = '-h' ] || [ "$1" = '--help' ]; then 67 | usage 68 | exit 0 69 | fi 70 | } 71 | 72 | # Print usage message on stdout by parsing start of script comments 73 | usage() { 74 | local help_msg 75 | help_msg=$(grep '^#/' "${script_dir}/${script_name}" | sed 's/^#\/\s\{,1\}//') 76 | echo "${help_msg}" 77 | 78 | list_available_vms 79 | } 80 | 81 | list_available_vms() { 82 | info "Available VMs" 83 | vboxmanage list vms 84 | } 85 | 86 | # Usage: is_a_vm VM 87 | # Predicate that checks whether the specified VM exists in VirtualBox 88 | is_a_vm() { 89 | local vm="${1}" 90 | vboxmanage list vms | grep --quiet "${vm}" 91 | } 92 | 93 | # Usage: is_vm_running VM 94 | # Predicate that checks whether the specified VM is running 95 | is_vm_running() { 96 | local vm="${1}" 97 | vboxmanage showvminfo "${vm}" | grep --quiet "running (since" 98 | } 99 | 100 | # Usage: list_shared_folders VM 101 | # Lists shared folders of the specified VM 102 | list_shared_folders() { 103 | local vm="${1}" 104 | 105 | vboxmanage showvminfo "${vm}" \ 106 | | grep '^Name:.*Host path:' \ 107 | | awk '{print $2}' \ 108 | | tr -d "'," 109 | } 110 | 111 | # Usage: delete_shared_folders VM 112 | # Deletes any shared folders the specified VM has 113 | delete_shared_folders() { 114 | info "Removing shares (if any)" 115 | local vm="${1}" 116 | local shares 117 | shares=$(list_shared_folders "${vm}") 118 | 119 | for share in ${shares}; do 120 | info "share ${share}" 121 | vboxmanage sharedfolder remove "${vm}" --name "${share}" 122 | done 123 | } 124 | 125 | # Usage: unique_ova_name VM 126 | # Generates a unique name for the .ova file 127 | # The default format is VMNAME-YYYY-MM-DD.ova, if that name already exists, the 128 | # format is VMNAME-YYYY-MM-DD-N.ova, with N = 1, 2, ... 129 | unique_ova_name() { 130 | local vm="${1}" 131 | local base_name="${vm}-${today}" 132 | 133 | if [ ! -f "${base_name}.ova" ]; then 134 | echo "${base_name}.ova" 135 | else 136 | local num=1 137 | while [ -f "${base_name}-${num}.ova" ]; do 138 | ((num++)) 139 | done 140 | echo "${base_name}-${num}.ova" 141 | fi 142 | } 143 | 144 | # Usage: info [ARG]... 145 | # 146 | # Prints all arguments on the standard output stream 147 | info() { 148 | printf "${yellow}>>> %s${reset}\n" "${*}" 149 | } 150 | 151 | # Usage: debug [ARG]... 152 | # 153 | # Prints all arguments on the standard output stream 154 | debug() { 155 | printf "${cyan}### %s${reset}\n" "${*}" 156 | } 157 | 158 | # Usage: error [ARG]... 159 | # 160 | # Prints all arguments on the standard error stream 161 | error() { 162 | printf "${red}!!! %s${reset}\n" "${*}" 1>&2 163 | } 164 | 165 | #}}} 166 | # {{{ Command line parsing 167 | 168 | #}}} 169 | 170 | main "${@}" 171 | -------------------------------------------------------------------------------- /bin/atb-init.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | #/ Usage: atb-init PROJECT_NAME [ROLE]... 6 | #/ atb-init -h|--help 7 | #/ 8 | #/ Initialises a Vagrant + Ansible project based and, optionally, installs 9 | #/ the specified roles from Ansible Galaxy. The project skeleton code is 10 | #/ downloaded from https://github.com/bertvv/ansible-skeleton 11 | #/ 12 | #/ OPTIONS 13 | #/ -h, --help 14 | #/ Print this help message 15 | #/ 16 | #/ EXAMPLES 17 | #/ atb-init sandbox 18 | #/ atb-init lampstack bertvv.mariadb bertvv.httpd 19 | # 20 | # Dependencies: 21 | # - ansible-galaxy 22 | # - git 23 | # - unzip 24 | # - wget 25 | 26 | #{{{ Bash settings 27 | set -o errexit # abort on nonzero exitstatus 28 | set -o nounset # abort on unbound variable 29 | set -o pipefail # don't hide errors within pipes 30 | #}}} 31 | #{{{ Variables 32 | readonly script_name=$(basename "${0}") 33 | readonly script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 34 | IFS=$'\t\n' # Split on newlines and tabs (but not on spaces) 35 | 36 | # Color definitions 37 | readonly reset='\e[0m' 38 | readonly cyan='\e[0;36m' 39 | readonly red='\e[0;31m' 40 | readonly yellow='\e[0;33m' 41 | # Debug info ('on' to enable) 42 | readonly debug='on' 43 | 44 | # Download location of skeleton code 45 | readonly download_url='https://github.com/bertvv/ansible-skeleton/archive' 46 | readonly skeleton_code_archive='master.zip' 47 | #}}} 48 | 49 | main() { 50 | check_cli_options "${@}" 51 | initialize_project_dir "${1}" 52 | shift 53 | install_roles "${@}" 54 | } 55 | 56 | #{{{ Helper functions 57 | 58 | check_cli_options() { 59 | if [ "$#" -eq '0' ]; then 60 | error "Expected at least 1 argument" 61 | usage 62 | exit 2 63 | elif [ "${1}" = '-h' ] || [ "${1}" = '--help' ]; then 64 | usage 65 | exit 0 66 | fi 67 | } 68 | 69 | # Usage: initialize_project_dir DIRECTORY 70 | # 71 | # Initialize a project directory from skeleton code 72 | initialize_project_dir() { 73 | local project="${1}" 74 | 75 | if [ -d "${project}" ]; then 76 | error "Project directory ${project} already exists. Bailing out." 77 | exit 1 78 | fi 79 | 80 | log "Downloading skeleton code" 81 | wget "${download_url}/${skeleton_code_archive}" 82 | unzip "${skeleton_code_archive}" 83 | rm "${skeleton_code_archive}" 84 | mv ansible-skeleton-master "${project}" 85 | 86 | log "Initializing Git repository" 87 | cd "${project}" 88 | git init 89 | git add . 90 | git commit --message "Initial commit from Ansible skeleton" 91 | mkdir ansible/host_vars/ 92 | } 93 | 94 | # Usage: install_roles [ROLE_NAME]... 95 | # 96 | # Iterate over the specified roles (if any) and call the install function for 97 | # each 98 | install_roles() { 99 | for role in "${@}"; do 100 | install_role "${role}" 101 | done 102 | } 103 | 104 | # Usage: install_role ROLE_NAME 105 | # 106 | # Install the specified role from Ansible Galaxy, or, failing that, from 107 | # Github. 108 | install_role() { 109 | local role_name="${1}" 110 | 111 | log "Installing ${role_name} from Ansible Galaxy" 112 | if ! ansible-galaxy install -p ansible/roles "${role_name}"; then 113 | 114 | log "This role does not seem to be on Ansible Galaxy." 115 | local user=${1%%\.*} 116 | local role=${1##*\.} 117 | local git_url="https://github.com/${user}/ansible-role-${role}.git" 118 | 119 | log "Trying to clone role from ${git_url}" 120 | if ! git clone "${git_url}" "ansible/roles/${role_name}"; then 121 | 122 | log "This does not seem to be an existing Github repo" 123 | local git_url="https://github.com/${user}/ansible-${role}.git" 124 | 125 | log "Trying to clone role from ${git_url}" 126 | if ! git clone "${git_url}" "ansible/roles/${role_name}"; then 127 | log "Cloning failed, skipping this role" 128 | fi 129 | fi 130 | fi 131 | } 132 | 133 | # Print usage message on stdout by parsing start of script comments 134 | usage() { 135 | grep '^#/' "${script_dir}/${script_name}" | sed 's/^#\/\w*//' 136 | } 137 | 138 | # Usage: log [ARG]... 139 | # 140 | # Prints all arguments on the standard output stream 141 | log() { 142 | printf "${yellow}>>> %s${reset}\\n" "${*}" 143 | } 144 | 145 | # Usage: debug [ARG]... 146 | # 147 | # Prints all arguments on the standard output stream, 148 | # if debug output is enabled 149 | debug() { 150 | [ "${debug}" != 'on' ] || printf "${cyan}### %s${reset}\\n" "${*}" 151 | } 152 | 153 | # Usage: error [ARG]... 154 | # 155 | # Prints all arguments on the standard error stream 156 | error() { 157 | printf "${red}!!! %s${reset}\\n" "${*}" 1>&2 158 | } 159 | 160 | #}}} 161 | 162 | main "${@}" 163 | 164 | -------------------------------------------------------------------------------- /bin/atb-role-deps.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | # Install role dependencies. This script is meant to be used in the context of 6 | # ansible-skeleton[1] and ansible-role-skeleton[2]. 7 | # 8 | # This script will search ansible/site.yml (or the specified playbook) for 9 | # roles assigned to hosts in the form "user.role". It will then try to install 10 | # them all. If possible, it will use Ansible Galaxy (on Linux, MacOS), but if 11 | # this is not available (e.g. on Windows), it will use Git to clone the latest 12 | # revision. 13 | # 14 | # Remark that this is a very crude technique and especially the Git fallback 15 | # is very brittle. It will download HEAD, and not necessarily the latest 16 | # release of the role. Additionally, the name of the repository is guessed, 17 | # but if it does not exist, the script will fail. 18 | # 19 | # Using ansible-galaxy and a dependencies.yml file is the best method, but 20 | # unavailable on Windows. This script is an effort to have a working 21 | # alternative. 22 | # 23 | # [1] https://github.com/bertvv/ansible-skeleton 24 | # [2] https://github.com/bertvv/ansible-role-skeleton 25 | 26 | set -o errexit # abort on nonzero exitstatus 27 | set -o nounset # abort on unbound variable 28 | set -o pipefail # don’t hide errors within pipes 29 | 30 | #{{{ Variables 31 | readonly SCRIPT_NAME=$(basename "${0}") 32 | 33 | playbook=ansible/site.yml 34 | roles_path=ansible/roles 35 | #}}} 36 | 37 | main() { 38 | local dependencies 39 | 40 | process_args "${@}" 41 | 42 | set_roles_path 43 | select_installer 44 | 45 | dependencies="$(find_dependencies)" 46 | 47 | for dep in ${dependencies}; do 48 | owner=${dep%%.*} 49 | role=${dep##*.} 50 | 51 | if [[ ! -d "${roles_path}/${dep}" ]]; then 52 | ${installer} "${owner}" "${role}" 53 | else 54 | echo "+ Skipping ${dep}, seems to be installed already" 55 | fi 56 | done 57 | } 58 | 59 | #{{{ Helper functions 60 | 61 | # If the default roles path does not exist, try "roles/" 62 | set_roles_path() { 63 | if [ ! -d "${roles_path}" ]; then 64 | roles_path="roles" 65 | fi 66 | } 67 | 68 | # Find dependencies in the specified playbook 69 | find_dependencies() { 70 | grep ' - .*\..*' "${playbook}" \ 71 | | cut --characters=7- \ 72 | | sort --unique 73 | } 74 | 75 | # Check if command line arguments are valid 76 | process_args() { 77 | if [ "${#}" -gt "1" ]; then 78 | echo "Expected at most 1 argument, got ${#}" >&2 79 | usage 80 | exit 2 81 | elif [ "${#}" -eq "1" ]; then 82 | if [ "${1}" = '-h' -o "${1}" = '--help' ]; then 83 | usage 84 | exit 0 85 | elif [ ! -f "${1}" ]; then 86 | echo "Playbook ‘${1}’ not found." >&2 87 | usage 88 | exit 1 89 | else 90 | playbook="${1}" 91 | fi 92 | elif [ "${#}" -eq "0" -a ! -f "${playbook}" ]; then 93 | cat << _EOF_ 94 | Default playbook ${playbook} not found. Maybe you should cd to the 95 | directory above ${playbook%%/*}/, or specify the playbook. 96 | _EOF_ 97 | usage 98 | exit 1 99 | fi 100 | } 101 | 102 | # Print usage message on stdout 103 | usage() { 104 | cat << _EOF_ 105 | Usage: ${SCRIPT_NAME} [PLAYBOOK] 106 | 107 | Installs role dependencies found in the specified playbook (or ${playbook} 108 | if none was given). 109 | 110 | OPTIONS: 111 | 112 | -h, --help Prints this help message and exits 113 | 114 | EXAMPLES: 115 | 116 | $ ${SCRIPT_NAME} 117 | $ ${SCRIPT_NAME} test.yml 118 | _EOF_ 119 | } 120 | 121 | 122 | # Usage: select_installer 123 | # Sets the variable `installer`, the function to use when installing roles 124 | # Try to use ansible-galaxy when it is available, and fall back to `git clone` 125 | # when it is not. 126 | select_installer() { 127 | if which ansible-galaxy > /dev/null 2>&1 ; then 128 | installer=install_role_galaxy 129 | else 130 | installer=install_role_git 131 | fi 132 | } 133 | 134 | # Usage: is_valid_url URL 135 | # returns 0 if the URL is valid, 22 otherwise 136 | is_valid_url() { 137 | local url=$1 138 | 139 | curl --silent --fail "${url}" > /dev/null 140 | } 141 | 142 | # Usage: install_role_galaxy OWNER ROLE 143 | install_role_galaxy() { 144 | local owner=$1 145 | local role=$2 146 | ansible-galaxy install --roles-path="${roles_path}" \ 147 | "${owner}.${role}" 148 | } 149 | 150 | # Usage: install_role_git OWNER ROLE 151 | install_role_git() { 152 | local owner=$1 153 | local role=$2 154 | 155 | # First try https://github.com/OWNER/ansible-role-ROLE 156 | local repo="https://github.com/${owner}/ansible-role-${role}" 157 | 158 | if is_valid_url "${repo}"; then 159 | git clone "${repo}" "${roles_path}/${owner}.${role}" 160 | else 161 | # If that fails, try https://github.com/OWNER/ansible-ROLE 162 | git clone "https://github.com/${owner}/ansible-${role}" \ 163 | "${roles_path}/${owner}.${role}" 164 | fi 165 | } 166 | #}}} 167 | 168 | main "${@}" 169 | -------------------------------------------------------------------------------- /bin/atb-init-role.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # Author: Bert Van Vreckem 4 | # 5 | #/ Usage: atb-init-role [OPTIONS]... ROLE 6 | #/ 7 | #/ Generates scaffolding code for an Ansible role based on 8 | #/ https://github.com/bertvv/ansible-role-skeleton. 9 | #/ 10 | #/ A git repo is initialized and the initial code is committed. 11 | #/ 12 | #/ OPTIONS 13 | #/ -h, --help 14 | #/ Print this help message and exit 15 | #/ 16 | #/ -t, --tests=TESTENV... 17 | #/ Initialize the specified test environment(s) for a new or 18 | #/ existing role. Possible values: docker,vagrant 19 | #/ Warning: no spaces are allowed in this option! 20 | #/ 21 | #/ EXAMPLES 22 | #/ atb-init-role prometheus 23 | #/ atb-init-role -t=vagrant hosts 24 | #/ atb-init-role --tests=docker zabbix 25 | #/ atb-init-role --tests=docker,vagrant nginx 26 | 27 | #{{{ Bash settings 28 | set -o errexit # abort on nonzero exitstatus 29 | set -o nounset # abort on unbound variable 30 | set -o pipefail # don't hide errors within pipes 31 | #}}} 32 | #{{{ Variables 33 | readonly script_name=$(basename "${0}") 34 | readonly script_dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 35 | IFS=$'\t\n' # Split on newlines and tabs (but not on spaces) 36 | 37 | # Color definitions 38 | readonly reset='\e[0m' 39 | readonly cyan='\e[0;36m' 40 | readonly red='\e[0;31m' 41 | readonly yellow='\e[0;33m' 42 | # Debug log ('on' to enable) 43 | readonly debug='on' 44 | 45 | # Script configuration variables 46 | readonly role_skeleton='https://github.com/bertvv/ansible-role-skeleton' 47 | readonly skeleton_download_dir="$(mktemp --directory)" 48 | 49 | test_env='none' # which test environment(s) should be initialized 50 | role_name='' 51 | role_dir='' 52 | #}}} 53 | 54 | main() { 55 | process_parameters "${@}" 56 | init_role_dir 57 | setup_test_environments "${test_env}" 58 | cleanup 59 | } 60 | 61 | #{{{ process_parameters() 62 | # Process command line parameters and set script configuration variables 63 | process_parameters() { 64 | while [ "$#" -gt 0 ]; do 65 | case "${1}" in 66 | -h|--help) 67 | usage 68 | exit 0 69 | ;; 70 | -t=*|--tests=*) 71 | test_env="${1##*=}" 72 | debug "Test environment initialization requested: ${test_env}" 73 | shift 74 | ;; 75 | -*) 76 | error "Unknown option: ${1}" 77 | usage 78 | exit 2 79 | ;; 80 | *) 81 | role_name="${1}" 82 | break 83 | ;; 84 | esac 85 | done 86 | 87 | if [ x"${role_name}" = x'' ]; then 88 | error "No role name specified!" 89 | usage 90 | exit 2 91 | fi 92 | 93 | # Check if we're inside the role dir and move out of it if necessary 94 | local current_dir="${PWD##*/}" 95 | if [ "${current_dir}" = "${role_name}" ]; then 96 | cd .. 97 | fi 98 | role_dir="${PWD}/${role_name}" 99 | debug "Role directory: ${role_dir}" 100 | } 101 | #}}} 102 | #{{{ init_role_dir() 103 | # Usage: init_role_dir 104 | # 105 | # Initialize a directory with scaffolding code for the specified Ansible role. 106 | # If the directory already exists, this function does nothing. 107 | init_role_dir() { 108 | log "Initializing role directory ${role_dir}" 109 | 110 | # First check if the directory already exists 111 | if [ -d "${role_dir}" ]; then 112 | log "A directory with name ´${role_name}´ already exists. Skipping this step." 113 | return 114 | fi 115 | 116 | ensure_skeleton_code_is_downloaded 117 | 118 | log "Creating directory for the role: ${role_dir}" 119 | mkdir "${role_dir}" 120 | 121 | debug 'Copy the role skeleton code' 122 | rsync --archive --exclude '.git' "${skeleton_download_dir}/" "${role_dir}" 123 | 124 | 125 | debug "Setting year in the LICENSE file" 126 | sed --in-place --expression "s/YEAR/$(date +%Y)/" "${role_dir}/LICENSE.md" 127 | 128 | subst_role_name "${role_dir}" 129 | 130 | log "Initializing Git repository, first commit" 131 | pushd "${role_dir}" > /dev/null 2>&1 132 | git init --quiet 133 | git add . 134 | git commit --quiet --message "First commit from role skeleton" 135 | popd > /dev/null 2>&1 136 | } 137 | #}}} 138 | #{{{ ensure_skeleton_code_is_downloaded() 139 | ensure_skeleton_code_is_downloaded() { 140 | 141 | if [ ! -d "${skeleton_download_dir}/.git" ]; then 142 | debug "Downloading skeleton code from Github" 143 | git clone --quiet "${role_skeleton}" "${skeleton_download_dir}" 144 | fi 145 | 146 | } 147 | 148 | #}}} 149 | #{{{ setup_test_environments() 150 | # Usage: setup_test_environments ENVIRONMENT... 151 | # 152 | # With ENVIRONMENT one of none, vagrant, or docker, or a comma-separated list 153 | # of the desired test environments 154 | setup_test_environments() { 155 | local environments 156 | 157 | # Split comma separated environment names into an array 158 | IFS=',' read -ra environments <<< "${1}" 159 | 160 | for environment in "${environments[@]}"; do 161 | debug "Setting up test env: ${environment}" 162 | case "${environment}" in 163 | 'none') 164 | return 165 | ;; 166 | 'vagrant') 167 | setup_vagrant_environment 168 | ;; 169 | 'docker') 170 | setup_docker_environment 171 | ;; 172 | *) 173 | error "Test environment ${environment} is not defined, skipping" 174 | ;; 175 | esac 176 | done 177 | 178 | } 179 | 180 | #}}} 181 | #{{{ setup_docker_environment() 182 | # Usage: setup_docker_environment 183 | setup_docker_environment() { 184 | fetch_test_branch 'docker' 185 | 186 | if [ ! -f "${role_dir}/.travis.yml" ]; then 187 | debug 'Adding .travis.yml' 188 | cd "${role_dir}" 189 | cp docker-tests/.travis.yml . 190 | git add .travis.yml 191 | git commit --quiet --message \ 192 | 'Add .travis.yml' 193 | fi 194 | } 195 | 196 | #}}} 197 | #{{{ setup_vagrant_environment() 198 | # Usage: setup_vagrant_environment 199 | setup_vagrant_environment() { 200 | fetch_test_branch 'vagrant' 201 | 202 | debug 'Create subdirectory for roles used in the test playbook' 203 | if [ ! -d "${role_dir}/vagrant-tests/roles" ]; then 204 | debug 'Making role available in the test environment' 205 | pushd "${role_dir}/vagrant-tests/" > /dev/null 2>&1 206 | mkdir roles 207 | 208 | debug 'Link from roles/ to the project root' 209 | ln --symbolic --force --no-dereference '../..' "roles/${role_name}" 210 | git add . 211 | git commit --quiet --message \ 212 | 'Make role under test available in test environment' 213 | fi 214 | } 215 | 216 | #}}} 217 | #{{{ fetch_test_branch() 218 | # Usage: fetch_test_branch ENVIRONMENT 219 | # 220 | # with ENVIRONMENT one of ‘docker’ or ‘vagrant’ 221 | fetch_test_branch() { 222 | local environment="${1}" 223 | local test_branch="${environment}-tests" 224 | 225 | if [ -d "${role_dir}/${test_branch}" ]; then 226 | log "The test environment ${test_branch} already exists, skipping" 227 | return 228 | fi 229 | 230 | log "Setting up ${environment} test environment" 231 | debug "Fetching test branch ${test_branch} from Github" 232 | 233 | debug 'Create empty branch for the test code' 234 | pushd "${role_dir}" > /dev/null 2>&1 235 | debug "Create empty branch for test code in ${role_dir}" 236 | git checkout --quiet --orphan "${test_branch}" 237 | git rm -r --force --quiet . 238 | popd > /dev/null 2>&1 239 | 240 | debug 'Copy test code from skeleton' 241 | ensure_skeleton_code_is_downloaded 242 | pushd "${skeleton_download_dir}" > /dev/null 2>&1 243 | 244 | debug "Copy test code from ${skeleton_download_dir}" 245 | git fetch --quiet origin "${test_branch}" 246 | git checkout --quiet "${test_branch}" 247 | rsync --archive --exclude '.git' \ 248 | "${skeleton_download_dir}/" \ 249 | "${role_dir}" 250 | popd > /dev/null 2>&1 251 | 252 | subst_role_name "${role_dir}" 253 | 254 | debug 'Set up the test branch, commit' 255 | pushd "${role_dir}" > /dev/null 2>&1 256 | debug "Committing test code" 257 | git add . 258 | git commit --quiet --message \ 259 | "Set up ${environment} test branch from skeleton code" 260 | 261 | debug "Creating worktree for test branch in the master branch" 262 | git checkout --quiet master 2> /dev/null 263 | debug "Adding worktree" 264 | git worktree add "${test_branch}" "${test_branch}" > /dev/null 2>&1 265 | popd > /dev/null 2>&1 266 | } 267 | 268 | #}}} 269 | #{{{ subst_role_name() 270 | # Usage: subst_role_name DIR 271 | # Replace placeholder ROLENAME with the actual role name in the 272 | # specified directory 273 | subst_role_name() { 274 | local dir="${1}" 275 | 276 | debug "Replacing ROLENAME in ${dir} with actual role name ${role_name}" 277 | find "${dir}" -type f -exec sed --in-place \ 278 | --expression "s/ROLENAME/${role_name}/g" {} \; 279 | git add . 280 | } 281 | 282 | #}}} 283 | #{{{ cleanup() 284 | # Usage: cleanup 285 | # Delete temporary files 286 | cleanup() { 287 | debug 'Cleaning up' 288 | rm -rf "${skeleton_download_dir}" 289 | } 290 | 291 | #}}} 292 | #{{{ usage() 293 | # Print usage message on stdout by parsing start of script comments 294 | usage() { 295 | grep '^#/' "${script_dir}/${script_name}" | sed 's/^#\/\w*//' 296 | } 297 | #}}} 298 | #{{{ logging 299 | # Usage: log [ARG]... 300 | # 301 | # Prints all arguments on the standard output stream 302 | log() { 303 | printf "${yellow}>>> %s${reset}\\n" "${*}" 304 | } 305 | 306 | # Usage: debug [ARG]... 307 | # 308 | # Prints all arguments on the standard output stream, 309 | # if debug output is enabled 310 | debug() { 311 | [ "${debug}" != 'on' ] || printf "${cyan}### %s${reset}\\n" "${*}" 312 | } 313 | 314 | # Usage: error [ARG]... 315 | # 316 | # Prints all arguments on the standard error stream 317 | error() { 318 | printf "${red}!!! %s${reset}\\n" "${*}" 1>&2 319 | } 320 | #}}} 321 | 322 | main "${@}" 323 | 324 | --------------------------------------------------------------------------------