├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── VERSION └── app ├── bin └── bundle-playbook ├── etc └── ansible.cfg └── lib ├── bin-header.sh └── run-playbook.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | indent_style = tab 10 | indent_size = 2 11 | max_line_length = 100 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # If you find yourself ignoring temporary files generated by your text editor 3 | # or operating system, you probably want to add a global ignore instead: 4 | # git config --global core.excludesfile ~/.gitignore_global 5 | 6 | # OS Specifics 7 | *~ 8 | *.bak 9 | Thumbs.db 10 | desktop.ini 11 | .DS_Store 12 | 13 | # Build 14 | build 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Contributor Covenant Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers 24 | pledge to making participation in our project and our community a harassment-free experience for 25 | everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity 26 | and expression, level of experience, education, socio-economic status, nationality, personal 27 | appearance, race, religion, or sexual identity and orientation. 28 | 29 | ### Our Standards 30 | 31 | Examples of behavior that contributes to creating a positive environment include: 32 | 33 | * Using welcoming and inclusive language 34 | * Being respectful of differing viewpoints and experiences 35 | * Gracefully accepting constructive criticism 36 | * Focusing on what is best for the community 37 | * Showing empathy towards other community members 38 | 39 | Examples of unacceptable behavior by participants include: 40 | 41 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Public or private harassment 44 | * Publishing others' private information, such as a physical or electronic address, without explicit 45 | permission 46 | * Other conduct which could reasonably be considered inappropriate in a professional setting 47 | 48 | ### Our Responsibilities 49 | 50 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are 51 | expected to take appropriate and fair corrective action in response to any instances of unacceptable 52 | behavior. 53 | 54 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, 55 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or 56 | to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, 57 | threatening, offensive, or harmful. 58 | 59 | ### Scope 60 | 61 | This Code of Conduct applies within all project spaces, and it also applies when an individual is 62 | representing the project or its community in public spaces. Examples of representing a project or 63 | community include using an official project e-mail address, posting via an official social media 64 | account, or acting as an appointed representative at an online or offline event. Representation of a 65 | project may be further defined and clarified by project maintainers. 66 | 67 | ### Enforcement 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting 70 | the project team at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and 71 | will result in a response that is deemed necessary and appropriate to the circumstances. The project 72 | team is obligated to maintain confidentiality with regard to the reporter of an incident. Further 73 | details of specific enforcement policies may be posted separately. 74 | 75 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face 76 | temporary or permanent repercussions as determined by other members of the project's leadership. 77 | 78 | ### Attribution 79 | 80 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 81 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 82 | 83 | [homepage]: https://www.contributor-covenant.org 84 | 85 | For answers to common questions about this code of conduct, see 86 | https://www.contributor-covenant.org/faq 87 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Daniel Pereira 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY =: clean package rpm deb deps release 2 | .DEFAULT_GOAL := package 3 | .SILENT : clean package rpm deb deps release 4 | 5 | # Temporary paths for building the artifacts 6 | build_dir=build/pkg 7 | dist_dir=build/dist 8 | 9 | # Variables that are replaced in build time 10 | bin_path=/usr/bin 11 | lib_path=/usr/lib/ansible-bundler 12 | etc_path=/etc/ansible-bundler 13 | version=$(shell cat VERSION) 14 | 15 | # Package variables 16 | package_name=ansible-bundler 17 | package_license=3-Clause BSD 18 | package_vendor=Daniel Pereira 19 | package_maintainer= 20 | package_url=https://github.com/kriansa/ansible-bundler 21 | 22 | deps: 23 | docker pull docker.io/skandyla/fpm 24 | 25 | clean: 26 | rm -rf $(build_dir) $(dist_dir) 27 | rmdir $(shell dirname $(build_dir)) 2> /dev/null || true 28 | 29 | package: 30 | @test -d $(build_dir) && echo "Build dir already exists! Run make clean before." && exit 1 || true 31 | mkdir -p $(build_dir)/{etc,usr/lib} 32 | cp -r app/bin $(build_dir)/usr 33 | cp -r app/etc $(build_dir)/etc/ansible-bundler 34 | cp -r app/lib $(build_dir)/usr/lib/ansible-bundler 35 | sed -i'' \ 36 | -e 's#LIB_PATH=.*#LIB_PATH=$(lib_path)#' \ 37 | -e 's#ETC_PATH=.*#ETC_PATH=$(etc_path)#' \ 38 | -e 's#VERSION=.*#VERSION=$(version)#' \ 39 | -e 's/%VERSION%/$(version)/' \ 40 | $(build_dir)/usr/bin/bundle-playbook 41 | echo "Built package v$(version) on directory '$(build_dir)'" 42 | 43 | deb: 44 | test -d $(dist_dir) || mkdir -p $(dist_dir) 45 | docker run -it --rm -v "$(shell pwd):/mnt" --entrypoint '' skandyla/fpm \ 46 | /bin/bash -c 'fpm -n "$(package_name)" -s dir -t deb -v $(version) \ 47 | --config-files /etc/ansible-bundler/ansible.cfg --deb-no-default-config-files \ 48 | --license "$(package_license)" --vendor "$(package_vendor)" \ 49 | --maintainer "$(package_maintainer)" --url "$(package_url)" \ 50 | -p /mnt/$(dist_dir) -C /mnt/$(build_dir) . > /dev/null \ 51 | && chown $(shell id -u):$(shell id -g) /mnt/$(dist_dir)/*.deb' 52 | echo "DEB package build successfully into $(dist_dir)" 53 | 54 | rpm: 55 | test -d $(dist_dir) || mkdir -p $(dist_dir) 56 | docker run -it --rm -v "$(shell pwd):/mnt" --entrypoint '' skandyla/fpm \ 57 | /bin/bash -c 'fpm -n "$(package_name)" -s dir -t rpm -v $(version) \ 58 | --config-files /etc/ansible-bundler/ansible.cfg \ 59 | --license "$(package_license)" --vendor "$(package_vendor)" \ 60 | --maintainer "$(package_maintainer)" --url "$(package_url)" \ 61 | -p /mnt/$(dist_dir) -C /mnt/$(build_dir) . > /dev/null \ 62 | && chown $(shell id -u):$(shell id -g) /mnt/$(dist_dir)/*.rpm' 63 | echo "RPM package build successfully into $(dist_dir)" 64 | 65 | release: 66 | # Get only the artifacts for this release version 67 | $(eval rpm_package := $(shell ls $(dist_dir)/ansible-bundler-$(version)*.rpm)) 68 | $(eval deb_package := $(shell ls $(dist_dir)/ansible-bundler_$(version)*.deb)) 69 | 70 | # This task uses my own release helper, available here: 71 | # https://github.com/kriansa/dotfiles/blob/master/plugins/git/bin/git-release 72 | git release $(version) --sign --use-version-file --artifact="$(rpm_package)" --artifact="$(deb_package)" 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :package: Ansible Bundler 2 | 3 | Ansible Bundler embeds together a full playbook and its dependencies so you can run it it from a 4 | single binary on _any*_ computer, having just Python as a host dependency - you don't even need 5 | Ansible! Think of it as [`makeself`](https://makeself.io/) for Ansible playbooks. 6 | 7 | The closest that Ansible provides natively for this is `ansible-pull`, but it requires the host to 8 | have Ansible properly installed, and you need to manage the playbook location yourself. 9 | 10 | While playbooks were never meant to be used as standalone packages, Ansible offers tools to help on 11 | more complex deployments, such as [Tower](https://www.ansible.com/products/tower) and 12 | [AWX](https://github.com/ansible/awx) _(Tower's open-source upstream project)_. 13 | 14 | * Well, we currently only support Unix based OSes. 15 | 16 | ## Use case 17 | 18 | Ansible is awesome. It's so powerful and flexible that we can use from server provisioning to 19 | automating mundane tasks such as bootstraping [your own](https://github.com/kriansa/dotfiles) 20 | computer. 21 | 22 | One thing that it lacks though is the ability to be used for simple auto scaling deployments where 23 | you just want to pull a playbook and run it easily. Currently, you need to setup Ansible, ensure you 24 | have a repository to download the files, manage the right permissions to it and then run 25 | `ansible-pull`. This can get harder when you have more complex playbooks with several dependencies. 26 | 27 | Ansible Bundler makes these steps easier by having a single binary that takes care of setting up 28 | Ansible on the host and executing the playbook without having to do anything globally (such as 29 | installing ansible). You can simply pull the playbook binary and execute it right away. 30 | 31 | ## Usage 32 | 33 | ##### Generate a new self-contained playbook: 34 | 35 | ```shell 36 | $ bundle-playbook -f playbook.yml 37 | ``` 38 | 39 | ##### Run it on the host: 40 | 41 | ```shell 42 | $ ./playbook.run 43 | ``` 44 | 45 | > You will need Python on the host in order to run the final executable. :+1: 46 | 47 | ##### Advanced build 48 | 49 | ```shell 50 | $ bundle-playbook --playbook-file=playbook.yml \ 51 | --requirements-file=requirements.yml \ 52 | --vars-file=vars.yml \ 53 | --ansible-version=2.8.0 \ 54 | --python-package=boto3 \ 55 | --extra-deps=files 56 | ``` 57 | 58 | > By default, all files on `roles` folder in the same path as the playbook.yml are automatically 59 | > included. If you need more dependent files, you can specify them using `--extra-deps` (short 60 | > `-d`). 61 | 62 | Run `bundle-playbook --help` to get a list of all possible parameters. 63 | 64 | #### Binary interface 65 | 66 | The built playbook binary has a few options that you can use at runtime. Here are the options you 67 | can currently use: 68 | 69 | ``` 70 | --help Show this help message and exit 71 | --debug Run the packaged bundler with verbose logging 72 | --keep-temp Keep extracted files into the tempfolder after finishing. This is 73 | useful for debugging purposes 74 | -e , --extra-vars= 75 | Set additional variables as key=value or YAML/JSON, or a filename if 76 | prepended with @. You can pass this parameter multiple times. This will 77 | take precedence on the variables that have been previously defined on 78 | the packaged playbook. 79 | ``` 80 | 81 | ## Installation 82 | 83 | Currently you can download and install it using the pre-built packages that are available in RPM and 84 | DEB formats on [Github releases](https://github.com/kriansa/ansible-bundler/releases). They should 85 | work on most RHEL-based distros (CentOS, Fedora, Amazon Linux, etc) as well as on Debian-based 86 | distros (Ubuntu, Mint, etc). There's also a [AUR 87 | available](https://aur.archlinux.org/packages/ansible-bundler/) if you're using Arch. 88 | 89 | If your distro is not compatible with the prebuilt packages, please refer to [Building](#building) 90 | below. 91 | 92 | ## Building 93 | 94 | You will need Docker installed on your machine. When you have it installed, you can proceed 95 | installing the dependencies with: 96 | 97 | ```shell 98 | $ make deps 99 | ``` 100 | 101 | This is only required once. After that you're good to go. You can currently build the package in a 102 | directory structure that you can later copy to your root filesystem. This is very useful as a base 103 | for building OS packages for most package managers such as RPM or DEB. 104 | 105 | ```shell 106 | $ make 107 | ``` 108 | 109 | > The output will be at `build/pkg` 110 | 111 | In fact, we offer support for building `deb` and `rpm` artifacts out of the box: 112 | 113 | ```shell 114 | $ make deb rpm 115 | ``` 116 | 117 | > The output will be at `build/dist` 118 | 119 | ## Contributing 120 | 121 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would 122 | like to change. 123 | 124 | Please make sure to update tests as appropriate. For more information, please refer to 125 | [Contributing](CONTRIBUTING.md). 126 | 127 | ## License 128 | 129 | This project is licensed under the BSD 3-Clause License - see the [LICENSE.md](LICENSE.md) file for 130 | details. 131 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.10.2 2 | -------------------------------------------------------------------------------- /app/bin/bundle-playbook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Ansible Bundler v%VERSION% 4 | # 5 | # Builds a playbook and bundles it into a self-executable that you can run to get it applied to the 6 | # current (local) system. 7 | # 8 | # Usage: $ bundle-playbook -f playbook.yml [OPTIONS] 9 | 10 | help() { 11 | echo "Usage: bundle-playbook -f [-r ] [-v ] [-d files] [-o ]" 12 | echo 13 | echo "Options:" 14 | echo " -f, --playbook-file= The path to the playbook you want to build. You must also add" 15 | echo " all local role dependencies on the same folder as the playbook" 16 | echo " file." 17 | echo " -d, --extra-deps= (Optional) Adds extra dependencies, such as files that are read" 18 | echo " by the playbook. You can pass this parameter multiple times." 19 | echo " -r, --requirements-file= (Optional) The file that describes all your Ansible Galaxy" 20 | echo " external role/collection dependencies. By default, it looks for a" 21 | echo " requirements.yml file in the same folder as the playbook." 22 | echo " -v, --vars-file= (Optional) The file that contains extra variables for your" 23 | echo " playbook. It must NOT be encrypted by Ansible Vault." 24 | echo " -o, --output= (Optional) The output file name. By default, it writes to a file" 25 | echo " named .run" 26 | echo " -a, --ansible-version= (Optional) The version of ansible to use on the playbook bundle." 27 | echo " by default, uses the latest one from PyPI at runtime." 28 | echo " -p, --python-package= (Optional) Install additional python dependencies to your" 29 | echo " playbooks. Use the python requirements.txt format to add new" 30 | echo " packages (e.g. -p boto==1.2.0 -p botocore==3.1.0) - for more" 31 | echo " information, see pip documentation at:" 32 | echo " https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers" 33 | echo " You can pass this parameter multiple times." 34 | echo " -h, --help Show this message and exit." 35 | } 36 | 37 | main() { 38 | # Get global variables 39 | set_config_vars 40 | 41 | printf "\e[1mAnsible Bundler\e[0m - \e[2mv%s\e[0m\n\n" "$VERSION" 42 | 43 | declare playbook_path playbook_file requirements_file vars_file output_file 44 | declare ansible_version extra_python_deps=() extra_deps=() 45 | validate_parameters "$@" 46 | 47 | # Build! 48 | declare tmpdir && create_build_folder 49 | copy_playbook "$playbook_path" "$playbook_file" "$requirements_file" "$vars_file" "${extra_deps[@]}" 50 | install_dependencies "$requirements_file" 51 | add_python_requirements "$ansible_version" "${extra_python_deps[@]}" 52 | add_entrypoint 53 | compress_output "$output_file" 54 | 55 | echo "Done." 56 | } 57 | 58 | # These variables below are meant to be replaced at build time 59 | set_config_vars() { 60 | VERSION=$(cd "$(dirname "$0")/../../" && cat VERSION)-dev 61 | LIB_PATH=$(cd "$(dirname "$0")/../lib" && pwd) 62 | ETC_PATH=$(cd "$(dirname "$0")/../etc" && pwd) 63 | } 64 | 65 | validate_parameters() { 66 | while [ $# -gt 0 ]; do 67 | case "$1" in 68 | -f|--playbook-file) 69 | playbook_file=$2 70 | shift 2 71 | ;; 72 | --playbook-file=*) 73 | playbook_file=${1#*=} 74 | shift 1 75 | ;; 76 | 77 | -r|--requirements-file) 78 | requirements_file=$2 79 | shift 2 80 | ;; 81 | --requirements-file=*) 82 | requirements_file=${1#*=} 83 | shift 1 84 | ;; 85 | 86 | -v|--vars-file) 87 | vars_file=$2 88 | shift 2 89 | ;; 90 | --vars-file=*) 91 | vars_file=${1#*=} 92 | shift 1 93 | ;; 94 | 95 | -d|--extra-deps) 96 | extra_deps+=("$2") 97 | shift 2 98 | ;; 99 | --extra-deps=*) 100 | extra_deps+=("${1#*=}") 101 | shift 1 102 | ;; 103 | 104 | -a|--ansible-version) 105 | ansible_version=$2 106 | shift 2 107 | ;; 108 | --ansible-version=*) 109 | ansible_version=${1#*=} 110 | shift 1 111 | ;; 112 | 113 | -p|--python-package) 114 | extra_python_deps+=("$2") 115 | shift 2 116 | ;; 117 | --python-package=*) 118 | extra_python_deps+=("${1#*=}") 119 | shift 1 120 | ;; 121 | 122 | -o|--output) 123 | output_file=$2 124 | shift 2 125 | ;; 126 | --output=*) 127 | output_file=${1#*=} 128 | shift 1 129 | ;; 130 | 131 | -h|--help) 132 | help 133 | exit 134 | ;; 135 | 136 | *) 137 | error "Parameter $1 is invalid. Please use --help to see all available options." 138 | exit 1 139 | ;; 140 | esac 141 | done 142 | 143 | if [ -z "$playbook_file" ]; then 144 | error "You need to specify a playbook file! (use -f or --playbook-file)" 145 | fi 146 | 147 | if ! [ -r "$playbook_file" ]; then 148 | error "Playbook file is not readable!" 149 | fi 150 | 151 | if [ -n "$requirements_file" ] && ! [ -r "$requirements_file" ]; then 152 | error "The requirements file you specified is not readable!" 153 | fi 154 | 155 | if [ -n "$vars_file" ] && ! [ -r "$vars_file" ]; then 156 | error "The vars file you specified is not readable!" 157 | fi 158 | 159 | for item in "${extra_deps[@]}"; do 160 | if ! [ -r "$item" ]; then 161 | error "The specified dependency '$item' is not readable!" 162 | fi 163 | done 164 | 165 | playbook_path="$(dirname "$playbook_file")" 166 | 167 | # Set default requirements_file, if it exists in the same path as the playbook 168 | if [ -z "$requirements_file" ] && [ -r "$playbook_path/requirements.yml" ]; then 169 | requirements_file="$playbook_path/requirements.yml" 170 | fi 171 | 172 | # Set default requirements_file, if it exists in the same path as the playbook 173 | if [ -z "$output_file" ]; then 174 | playbook_basename="$(basename "${playbook_file%.*}")" 175 | output_file="$playbook_path/$playbook_basename.run" 176 | fi 177 | } 178 | 179 | error() { 180 | echo "$@" && exit 1 181 | } 182 | 183 | create_build_folder() { 184 | tmpdir="$(mktemp -d /tmp/ansible-bundler.XXXXX)" 185 | # shellcheck disable=SC2064 186 | trap "rm -rf '$tmpdir'" EXIT 187 | } 188 | 189 | copy_playbook() { 190 | local playbook_path=$1 191 | local playbook_file=$2 192 | local requirements_file=$3 193 | local vars_file=$4 194 | shift 4; local extra_deps=("${@}") 195 | 196 | echo "Adding playbook..." 197 | cp -L --preserve=timestamps "$playbook_file" "$tmpdir/playbook.yml" 198 | if [ -d "$playbook_path/roles" ]; then 199 | echo "Adding roles..." 200 | cp -Lr --preserve=timestamps "$playbook_path/roles" "$tmpdir" 201 | fi 202 | 203 | if [ -n "$requirements_file" ]; then 204 | echo "Adding requirements..." 205 | cp -L --preserve=timestamps "$requirements_file" "$tmpdir/requirements.yml" 206 | fi 207 | 208 | if [ -n "$vars_file" ]; then 209 | echo "Adding vars..." 210 | cp -L --preserve=timestamps "$vars_file" "$tmpdir/vars.yml" 211 | fi 212 | 213 | if [ ${#extra_deps[@]} -gt 0 ]; then 214 | echo "Adding extra dependencies..." 215 | for item in "${extra_deps[@]}"; do 216 | cp -Lr --preserve=timestamps "$item" "$tmpdir" 217 | done 218 | fi 219 | 220 | echo "Adding ansible config..." 221 | cp -L --preserve=timestamps "$ETC_PATH/ansible.cfg" "$tmpdir" 222 | } 223 | 224 | add_python_requirements() { 225 | local ansible_version=$1 226 | shift; local extra_packages=("${@}") 227 | 228 | if [ -n "$ansible_version" ]; then 229 | echo "ansible==$ansible_version" > "$tmpdir/requirements.txt" 230 | else 231 | echo "ansible" > "$tmpdir/requirements.txt" 232 | fi 233 | 234 | # Add extra python packages 235 | for package in "${extra_packages[@]}"; do 236 | echo "$package" >> "$tmpdir/requirements.txt" 237 | done 238 | } 239 | 240 | # Download required roles and collections, if any 241 | install_dependencies() { 242 | ! test -f "$tmpdir/requirements.yml" && return 243 | 244 | install_roles 245 | install_collections 246 | } 247 | 248 | install_collections() { 249 | # Prevent collections installs if there's no such key at the requirements.yml file 250 | # This will allow ansible-bundler to be backwards compatible with ansible < 2.9 when galaxy only 251 | # packaged roles and not collections. 252 | grep "collections:" "$tmpdir/requirements.yml" > /dev/null 2>&1 || return 253 | 254 | echo "Installing playbook collection dependencies..." 255 | 256 | # This line just prevents ansible-galaxy from complaining about the argument --collections-path 257 | # not being at the global collections path. This is totally harmless for this purpose. 258 | export ANSIBLE_COLLECTIONS_PATHS="$tmpdir/galaxy-collections" 259 | 260 | ansible-galaxy collection install --ignore-errors --force-with-deps --verbose \ 261 | --requirements-file="$tmpdir/requirements.yml" \ 262 | --collections-path="$tmpdir/galaxy-collections"; local status=$? 263 | 264 | if [ $status -ne 0 ]; then 265 | echo "Collection dependencies installation failed." 266 | exit 1 267 | fi 268 | } 269 | 270 | install_roles() { 271 | echo "Installing playbook role dependencies..." 272 | 273 | # This line just prevents ansible-galaxy from complaining about the argument --roles-path 274 | # not being at the global roles path. This is totally harmless for this purpose. 275 | export ANSIBLE_ROLES_PATH="$tmpdir/galaxy-roles" 276 | 277 | ansible-galaxy role install --ignore-errors --force-with-deps --verbose \ 278 | --role-file="$tmpdir/requirements.yml" \ 279 | --roles-path="$tmpdir/galaxy-roles"; local status=$? 280 | 281 | # Remove galaxy install metadata, which is dynamically generated, but non-deterministic and 282 | # prevents our build output to be idempotent 283 | rm "$tmpdir"/galaxy-roles/*/*/.galaxy_install_info 2> /dev/null 284 | 285 | if [ $status -ne 0 ]; then 286 | echo "Role dependencies installation failed." 287 | exit 1 288 | fi 289 | } 290 | 291 | add_entrypoint() { 292 | echo "Adding entrypoint..." 293 | cp -L --preserve=timestamps "$LIB_PATH/run-playbook.sh" "$tmpdir" 294 | chmod 0755 "$tmpdir/run-playbook.sh" 295 | } 296 | 297 | compress_output() { 298 | local output_file=$1 299 | 300 | echo "Packaging files to the output..." 301 | 302 | # First add the header to the final script 303 | cat "$LIB_PATH/bin-header.sh" > "$output_file" 304 | 305 | # Do some build-time metadata insertion 306 | sed -i'' \ 307 | -e "s/UNCOMPRESS_SKIP=.*/UNCOMPRESS_SKIP=$(( $(wc -l < "$output_file") + 1 ))/" \ 308 | -e "s/\$VERSION/$VERSION/g" \ 309 | "$output_file" 310 | 311 | # Ensure all files and directories mtime & atime dates to a specific point in time so we ensure 312 | # the tar process will be idempotent given the same files are built 313 | find "$tmpdir" -exec touch -d "1970-01-01T00:00:00Z" {} + 314 | 315 | # Then add the tar.gz binary content to it 316 | tar czC "$tmpdir" . >> "$output_file" 317 | 318 | # Lastly, make it executable 319 | chmod +x "$output_file" 320 | 321 | echo "Bundle successfully packaged to $output_file" 322 | } 323 | 324 | main "$@" 325 | -------------------------------------------------------------------------------- /app/etc/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | retry_files_enabled = False 3 | interpreter_python = auto_silent 4 | roles_path = galaxy-roles 5 | collections_paths = galaxy-collections 6 | -------------------------------------------------------------------------------- /app/lib/bin-header.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is a packaged ansible playbook file using Ansible Bundler v$VERSION. 4 | # You can run this with --debug to show more information about the process. 5 | 6 | # This is how many lines we need to skip to consider this file a binary tar.gz - this value is 7 | # calculated at build-time, so we just need to keep this placeholder here. 8 | UNCOMPRESS_SKIP=0 9 | 10 | help() { 11 | echo "Usage: $0 [OPTIONS]" 12 | echo 13 | echo "This is a playbook packaged using Ansible Bundler v$VERSION" 14 | echo 15 | echo "Options:" 16 | echo " --help Show this help message and exit" 17 | echo " --debug Run the packaged bundler with verbose logging" 18 | echo " --keep-temp Keep extracted files into the tempfolder after finishing. This is" 19 | echo " useful for debugging purposes" 20 | echo " -e , --extra-vars=" 21 | echo " Set additional variables as key=value or YAML/JSON, or a filename if" 22 | echo " prepended with @. You can pass this parameter multiple times. This will" 23 | echo " take precedence on the variables that have been previously defined on" 24 | echo " the packaged playbook." 25 | } 26 | 27 | main() { 28 | args="" 29 | 30 | while [ $# -gt 0 ]; do 31 | case "$1" in 32 | # Show debug logs 33 | --debug) DEBUG=1 && shift ;; 34 | 35 | # Keep extracted files into the tempfolder. Useful for debugging 36 | --keep-temp) KEEP_TEMP=1 && shift ;; 37 | 38 | # Passthrough directly to the run-playbook.sh 39 | -e|--extra-vars) 40 | args="$args --extra-vars \"$(escape_quotes "$2")\"" 41 | shift 2 42 | ;; 43 | --extra-vars=*) 44 | args="$args --extra-vars \"$(escape_quotes "${1#*=}")\"" 45 | shift 1 46 | ;; 47 | 48 | # Show help message 49 | --help|-h) help && exit ;; 50 | 51 | # Ignore all other parameters 52 | *) invalid_parameter_error "$1" && exit 1 ;; 53 | esac 54 | done 55 | 56 | # Trick to get the params parsed correctly on POSIX shell. I would much love not to have this kind 57 | # of sorcery in the code and just use Bash arrays, but then it would be hard to be compatible with 58 | # BSD. Anyway, what this does is that it will set the positional arguments ($1, $2, $3, etc) to 59 | # the ones set in $extra_params using the escaped variables that we got from CLI. The function 60 | # escape_quotes plays an essential role here, because it will ensure that double quotes coming 61 | # from user input will be escaped properly. Then, we will use the $@ below with the parameters 62 | # correctly assigned as ansible-playbook args. 63 | eval "set -- $args" 64 | 65 | create_tmpfolder 66 | extract_content 67 | run_entrypoint "$@" 68 | } 69 | 70 | # Escapes any double quotes with backslashes 71 | escape_quotes() { 72 | printf '%s' "$1" | sed -E 's/"/\\"/g' 73 | } 74 | 75 | invalid_parameter_error() { 76 | param=$1 77 | 78 | echo "Invalid parameter $param" 79 | echo "Please use $0 --help to see all available options." 80 | } 81 | 82 | create_tmpfolder() { 83 | tmpdir="$(mktemp -d "/tmp/ansible-bundle.XXXXX")" 84 | 85 | if [ -n "$KEEP_TEMP" ]; then 86 | trap "log 'Done.'" EXIT 87 | else 88 | # shellcheck disable=SC2064 89 | trap "log 'Finished, removing temp content...'; rm -rf '$tmpdir'; log 'Done.'" EXIT 90 | fi 91 | } 92 | 93 | extract_content() { 94 | log "Extracting bundle contents to ${tmpdir}..." 95 | 96 | # Ensure we are compatible with both bsd and GNU tar 97 | extra_params="" 98 | tar --version | grep 'GNU tar' > /dev/null 2>&1 && extra_params="--warning=no-timestamp" 99 | 100 | tail -n +$UNCOMPRESS_SKIP "$0" | tar xzC "$tmpdir" $extra_params 101 | } 102 | 103 | run_entrypoint() { 104 | log "Running entrypoint..." 105 | BASEDIR="$tmpdir" "$tmpdir/run-playbook.sh" "$@" 106 | } 107 | 108 | log() { 109 | test -n "$DEBUG" && echo "[$(date '+%Y-%m-%d %H:%I:%S %Z')]" "$@" 110 | } 111 | 112 | # Ensure we run main and exit afterwards, so we don't end up reading garbage 113 | main "$@" 114 | exit 115 | 116 | # Below this line is the content of the compressed ansible playbook. 117 | -------------------------------------------------------------------------------- /app/lib/run-playbook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is the entry point for the playbook bundle. It has a hard dependency on Python. It will try 4 | # to look for an already existing installation of Ansible, and will try to install it if not found. 5 | # 6 | # Then, it will run the playbook.yml locally file using ansible. 7 | 8 | main() { 9 | # You must call this using BASEDIR so it can locate the right path where the temp bundle was 10 | # extracted. If you don't, it will use the current working dir (cwd). 11 | export BASEDIR=${BASEDIR:-.} 12 | 13 | # Ensure we have HOME defined, otherwise set it manually 14 | test -z "$HOME" && export HOME; HOME="$(getent passwd "$(id -un)" | cut -d: -f6)" 15 | 16 | export PIP_ROOT_PATH; PIP_ROOT_PATH="$(realpath "${BASEDIR}/python-deps")" 17 | export PATH="${PIP_ROOT_PATH}/usr/bin:${PIP_ROOT_PATH}${HOME}/.local/bin:${PATH}" 18 | 19 | ensure_python_is_installed 20 | install_ansible 21 | 22 | # Export the right paths so we can run python binaries installed on non-default paths 23 | export PYTHONPATH; PYTHONPATH=$(find "$PIP_ROOT_PATH" -type d -name site-packages | head -1) 24 | 25 | run_playbook "$@" 26 | } 27 | 28 | ensure_python_is_installed() { 29 | if ! command -v python > /dev/null 2>&1 && ! command -v python3 > /dev/null 2>&1; then 30 | echo "Error: Python is not installed!" 31 | exit 1 32 | fi 33 | 34 | if ! command -v pip > /dev/null 2>&1 && ! command -v pip3 > /dev/null 2>&1; then 35 | echo "Error: Python pip is not found!" 36 | exit 1 37 | fi 38 | } 39 | 40 | pip() { 41 | if command -v pip3 > /dev/null 2>&1; then 42 | command pip3 "$@" 43 | else 44 | command pip "$@" 45 | fi 46 | } 47 | 48 | # Install ansible if needed 49 | install_ansible() { 50 | echo "Installing playbook Python dependencies..." 51 | 52 | # Create package root path if non existent 53 | test -d "$PIP_ROOT_PATH" || mkdir "$PIP_ROOT_PATH" 54 | 55 | # We need to pass the absolute path to --root because there's a weird bug on PIP prevent 56 | # installing the bin directory when you just pass the relative path. 57 | # 58 | # Uses --no-cache-dir because in some memory constrained environments, pip tries to use too much 59 | # memory for the cache, which causes a MemoryError. 60 | # See: https://stackoverflow.com/questions/29466663/memory-error-while-using-pip-install-matplotlib 61 | pip install --requirement="$BASEDIR/requirements.txt" --no-cache-dir \ 62 | --user --root="$PIP_ROOT_PATH"; status=$? 63 | 64 | if [ $status -ne 0 ]; then 65 | echo "Error: Python dependencies could not be installed." 66 | exit 1 67 | fi 68 | } 69 | 70 | # Escapes any double quotes with backslashes 71 | escape_quotes() { 72 | printf '%s' "$1" | sed -E 's/"/\\"/g' 73 | } 74 | 75 | run_playbook() { 76 | extra_params="" 77 | 78 | # Add bundled extra-vars parameter if existent 79 | test -r "$BASEDIR/vars.yml" && extra_params="--extra-vars=@$BASEDIR/vars.yml" 80 | 81 | # Then add runtime extra-vars, if passed 82 | while [ $# -gt 0 ]; do 83 | case "$1" in 84 | -e|--extra-vars) 85 | extra_params="$extra_params --extra-vars \"$(escape_quotes "$2")\"" 86 | shift 2 87 | ;; 88 | --extra-vars=*) 89 | extra_params="$extra_params --extra-vars \"$(escape_quotes "${1#*=}")\"" 90 | shift 1 91 | ;; 92 | *) shift ;; 93 | esac 94 | done 95 | 96 | # Trick to get the params parsed correctly on POSIX shell. I would much love not to have this kind 97 | # of sorcery in the code and just use Bash arrays, but then it would be hard to be compatible with 98 | # BSD. Anyway, what this does is that it will set the positional arguments ($1, $2, $3, etc) to 99 | # the ones set in $extra_params using the escaped variables that we got from CLI. The function 100 | # escape_quotes plays an essential role here, because it will ensure that double quotes coming 101 | # from user input will be escaped properly. Then, we will use the $@ below with the parameters 102 | # correctly assigned as ansible-playbook args. 103 | eval "set -- $extra_params" 104 | 105 | # Run the playbook 106 | # shellcheck disable=SC2086 107 | ANSIBLE_CONFIG="$BASEDIR/ansible.cfg" ansible-playbook --inventory="localhost," \ 108 | --connection=local "$@" "$BASEDIR/playbook.yml" 109 | } 110 | 111 | main "$@" 112 | --------------------------------------------------------------------------------