Last active
January 6, 2024 23:51
-
-
Save thehunmonkgroup/1efccb1aca8ed023ba0e to your computer and use it in GitHub Desktop.
A script to quickly knock up and down Vagrant boxes, see http://xylil.com/2015/05/12/vagrant-crash-course-for-the-busy-developer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Spins up a quick Vagrant box. Run without arguments for help. | |
vagrant_dir="${HOME}/vagrant" | |
box_dir="temp" | |
ssh_port="2202" | |
ssh_pubkey_path="${HOME}/.ssh/id_rsa.pub" | |
custom_script="${HOME}/bin/custom-server-tools.sh" | |
vagrant_public_box_url="https://app.vagrantup.com/boxes/search" | |
usage_short() { | |
local program=`basename ${0}` | |
echo " | |
Usage: | |
# Help | |
${program} -h | |
# Box management | |
${program} -u [-p <boxes-dir>] | |
${program} -l [-p <boxes-dir>] | |
${program} -r [-p <boxes-dir>] | |
${program} -d [-p <boxes-dir>] | |
${program} -c [-p <boxes-dir>] [-k <ssh-pubkey-file>] [-s <custom-script>] [box-dir] [ssh-port] | |
" | |
} | |
usage() { | |
usage_short | |
echo "This script provides support for developers who need to create, start, stop | |
and remove lots of Vagrant boxes with ease. | |
It handles the most common extra tasks around creating a Vagrant box to get it | |
to a state of immediate use for local development, and provides an interface | |
to quickly tear things down when no longer needed. It also includes some simple | |
switches for selectively starting, stopping, and restarting VMs. | |
Note that you have to have added boxes to your local Vagrant install in order | |
for them to be available for quick creation. That can be accomplished by | |
running: | |
vagrant box add [box path] | |
Where box path is a full URL to a box, or a relative path to a box hosted in | |
the public catalog -- this is a very easy place to find all the common distros. | |
For example, to install this box: | |
https://app.vagrantup.com/bento/boxes/debian-8.9 | |
You would run: | |
vagrant box add bento/debian-8.9 | |
Their search page is a great place to start: | |
${vagrant_public_box_url} | |
The script only installs the most basic Vagrant config needed to get the box | |
running. From there, Vagrant-specific customizations can be made. | |
A created box has these additional janitorial tasks completed: | |
- Installs vagrant-vbguest plugin on host machine (auto Guest Additions updates) | |
- SELinux disabled if necessary. | |
- Sensible local hostname configured. | |
- Rsync and Vim installed. | |
- Root SSH access configured with a handy output of client-side SSH config. | |
- Optional custom script executed if SSH client-side config has been | |
pre-configured (very handy for loading additional customizations to the | |
VM). | |
Arguments: | |
-h: This help message. | |
-u: Bring a box up. A list will be provided from ${vagrant_dir}. | |
-l: Halt a box. A list will be provided from ${vagrant_dir}. | |
-r: Reload a box. A list will be provided from ${vagrant_dir}. | |
-d: Delete a box. A list will be provided from ${vagrant_dir}. | |
-c: Create a box. The box will be created in box-dir inside the | |
${vagrant_dir} directory. | |
box-dir: Directory to create the box under ${vagrant_dir}. Default is | |
'${box_dir}'. | |
ssh-port: Host port for SSH access. Default '${ssh_port}'. | |
-m: Select multiple boxes for the action (only works for up/reload/halt). | |
-p <path>: Override the base directory, default is '${vagrant_dir}'. | |
-k <filepath>: Path to SSH pubkey to insert into the box's root user | |
authorized_keys file. Default is '${ssh_pubkey_path}'. | |
-s <filepath>: Path to a custom script to execute if an SSH pubkey is | |
installed on the VM. Default is '${custom_script}'. You must have an | |
entry in .ssh/config where the Host name matches the box-dir name, or the | |
script will not execute. | |
CAVEATS: | |
- Most testing on latest releases of CentOS 6.x/7.x and Debian 7.x/8.x VMs, | |
should work for any RHEL or Debian variants, YMMV. | |
- Assumes 64-bit installations. | |
" | |
} | |
create_box() { | |
local full_path=${vagrant_dir}/${box_dir} | |
if [ -d "${full_path}" ]; then | |
echo "${full_path} already exists..." | |
_confirm_delete_box ${box_dir} | |
fi | |
local hostname="${box_dir}.local" | |
local box_list=`vagrant box list | awk '{print $1}'` | |
if [ -z "${box_list}" ]; then | |
echo " | |
No local boxes found! Only locally installed boxes are available for quick | |
install. Run 'vagrant box add <box name>' to install a box locally. A great | |
list of boxes can be found here: | |
${vagrant_public_box_url} | |
" | |
exit 1 | |
fi | |
PS3="Select box to deploy: " | |
select box in ${box_list}; do | |
mkdir -p $full_path | |
cd $full_path | |
# All Vagrant boxes must have a configuration file named Vagrantfile in | |
# the directory the box data will be saved. | |
# If 'vagrant init' is executed, a default Vagrantfile will be created in | |
# the directory where the command was executed. | |
# Here, we roll our own because of the custom SSH port. | |
cat > ${full_path}/Vagrantfile << EOF | |
Vagrant.configure(2) do |config| | |
config.vm.box = "${box}" | |
# Vagrant usually checks for versioned box updates, this disables the check. | |
config.vm.box_check_update = false | |
# Share SSH locally by default | |
config.vm.network :forwarded_port, | |
guest: 22, | |
host: ${ssh_port}, | |
id: "ssh" | |
# In case the vagrant-vbguest plugin is installed. | |
config.respond_to?(:vbguest) && config.vbguest.auto_update = false | |
# Uncomment this and edit as appropriate to add a shared folder. | |
#config.vm.synced_folder "/full/path/on/host/", "/full/path/on/vm/", owner: "root", group: "root" | |
config.vm.provider :virtualbox do |vb| | |
vb.customize ["modifyvm", :id, "--rtcuseutc", "on"] | |
# set timesync parameters to keep the clocks better in sync | |
# sync time every 10 seconds | |
vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-interval", 10000 ] | |
# adjustments if drift > 100 ms | |
vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-min-adjust", 100 ] | |
# sync time on restore | |
vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-on-restore", 1 ] | |
# sync time on start | |
vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-start", 1 ] | |
# at 1 second drift, the time will be set and not "smoothly" adjusted | |
vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 1000 ] | |
end | |
end | |
# vi: ft=ruby | |
EOF | |
# Start up a configured Vagrant VM. | |
vagrant up | |
echo "Upgrading kernel (if necessary)..." | |
vagrant ssh -- "test -f /usr/bin/yum && sudo yum -y update kernel*" | |
vagrant ssh -- "test -f /usr/bin/apt-get && sudo apt-get -q update && sudo apt-get -q -y install linux-image-amd64" | |
# No kernel updates for FreeBSD, freebsd-update can be run manually after | |
# install. | |
echo "Ensuring gcc/make/kernel-devel are installed..." | |
vagrant ssh -- "test -f /usr/bin/yum && sudo yum -y install gcc make kernel-devel" | |
vagrant ssh -- "test -f /usr/bin/apt-get && sudo apt-get -q -y install gcc make linux-headers-amd64" | |
echo "Resetting SELinux (if necessary)..." | |
# Execute an SSH command on the VM. The part after the double dash is | |
# what gets passed to the VM for execution. By default, it's executed | |
# as a non-privileged user named 'vagrant'. This user has sudo access. | |
# If 'vagrant ssh' is run with no arguments, an SSH connection will be | |
# opened to the box under the default user. | |
vagrant ssh -- "test -f /etc/selinux/config && sudo sed -i -e 's/^SELINUX=.*/SELINUX=permissive/g' /etc/selinux/config" | |
echo "Setting hostname..." | |
vagrant ssh -- "test -f /etc/sysconfig/network && sudo sed -i -e 's/^HOSTNAME=.*/HOSTNAME=${hostname}/g' /etc/sysconfig/network" | |
vagrant ssh -- "test -f /etc/hostname && echo ${hostname} | sudo tee /etc/hostname" | |
vagrant ssh -- "test -f /etc/rc.conf && su - root -c \"sed -i -e 's/^hostname=.*/hostname=${hostname}/g' /etc/rc.conf\"" | |
# Without these hostfile entries, you can get long delays while DNS tries | |
# to query for the host. | |
echo "Configuring /etc/hosts..." | |
vagrant ssh -- "echo '127.0.0.1 ${hostname} ${box_dir}' | sudo tee -a /etc/hosts" | |
echo "Activating vagrant-vbguest plugin..." | |
sed -i.bak "s/config\.vbguest\.auto_update = false$/config.vbguest.auto_update = true/" ${full_path}/Vagrantfile | |
rm ${full_path}/Vagrantfile.bak | |
echo "Checking for vagrant-vbguest plugin on host..." | |
vbguest_exists=`vagrant plugin list | grep vagrant-vbguest` | |
if [ -z "${vbguest_exists}" ]; then | |
echo "Installing vagrant-vbguest plugin on host..." | |
vagrant plugin install vagrant-vbguest | |
fi | |
echo "Rebooting server..." | |
# Restart the server. Shortcut for 'vagrant halt; vagrant up'. | |
vagrant reload | |
# Let's make sure there's a way to sync over files, and a basic editor in place. | |
echo "Installing basic packages..." | |
vagrant ssh -- "test -f /usr/bin/yum && sudo yum -y install rsync vim-enhanced" | |
vagrant ssh -- "test -f /usr/bin/apt-get && sudo apt-get -y install rsync vim" | |
# The fstab entry is nessesary for bash to be able to function in FreeBSD. | |
vagrant ssh -- "test -f /usr/sbin/pkg && su - root -c '/usr/bin/yes | pkg install rsync vim bash' && su - root -c 'echo \"fdesc /dev/fd fdescfs rw 0 0\" > /etc/fstab'" | |
# If the script finds a readable file at ${ssh_pubkey_path}, then it will | |
# copy it to the authorized_keys file for the root user on the VM. | |
if [ -r ${ssh_pubkey_path} ]; then | |
echo "Setting up root SSH access..." | |
local pubkey=`cat ${ssh_pubkey_path}` | |
vagrant ssh -- "sudo mkdir -m 700 /root/.ssh" | |
vagrant ssh -- "echo '${pubkey}' | sudo tee -a /root/.ssh/authorized_keys" | |
ssh_config_exists=`cat ${HOME}/.ssh/config | grep "^Host ${box_dir}$"` | |
# If the custom script is executable, and a Host entry matching | |
# ${box_dir} is found in the SSH config, execute the custom script. | |
if [ -x ${custom_script} ] && [ -n "${ssh_config_exists}" ]; then | |
echo "Executing ${custom_script}..." | |
${custom_script} ${box_dir} | |
fi | |
fi | |
break | |
done | |
echo " | |
SSH config. | |
Add the following to ${HOME}/.ssh/config for quick | |
root access to the server: | |
Host ${box_dir} | |
Hostname localhost | |
Port ${ssh_port} | |
User root | |
HostKeyAlias ${box_dir} | |
" | |
} | |
_delete_box() { | |
local box_dir=${1} | |
echo "Removing ${vagrant_dir}/${box_dir} virtual machine..." | |
cd ${vagrant_dir}/${box_dir} | |
# Delete the VM. --force overrides the 'Are you sure?' prompt. | |
vagrant destroy --force | |
cd ${vagrant_dir} | |
# Bit of defensive programming here, in case for some freaky reason | |
# ${box_dir} is empty, we don't want to wipe the entire vagrant dir. | |
if [ -n "${box_dir}" ]; then | |
rm -rf ${vagrant_dir}/${box_dir} | |
fi | |
echo "Removal complete." | |
} | |
_confirm_delete_box() { | |
local box_dir=${1} | |
echo -n "Are you sure you want to remove ${vagrant_dir}/${box_dir}? (y/N): " | |
read KILL_VM | |
if [ "${KILL_VM}" = "y" ]; then | |
_delete_box ${box_dir} | |
else | |
echo "User cancelled" | |
exit 0 | |
fi | |
} | |
_box_list() { | |
local all_boxes=`ls -1 ${vagrant_dir} | tr -d "/"` | |
echo "${all_boxes}" | |
} | |
_box_command() { | |
local cmd="${1}" | |
shift | |
local box_list=("$@") | |
for box_dir in "${box_list[@]}"; do | |
echo "Performing command '${cmd}' for box '${box_dir}'" | |
if [ -f "${vagrant_dir}/${box_dir}/Vagrantfile" ]; then | |
cd ${vagrant_dir}/${box_dir} | |
vagrant ${cmd} | |
else | |
echo "ERROR: ${vagrant_dir}/${box_dir} has no Vagrantfile" | |
fi | |
done | |
} | |
_check_valid_selection() { | |
box_list=("$@") | |
if [ ${#box_list[@]} -eq 0 ] || [ -z "${box_list[0]}" ]; then | |
echo "ERROR: Invalid selection" | |
return 1 | |
fi | |
} | |
multiselect() { | |
local -n final_choices=${1} | |
local action=${2} | |
local choices=() | |
local options=() | |
rebuild_choices() { | |
local selection_idx="${1}" | |
local new_array=() | |
local deleted= | |
for i in "${choices[@]}"; do | |
if [[ "${i}" = "${selection_idx}" ]]; then | |
deleted="1" | |
else | |
new_array+=(${i}) | |
fi | |
done | |
if [[ -z "${deleted}" ]]; then | |
new_array+=(${selection_idx}) | |
fi | |
choices=("${new_array[@]}") | |
} | |
get_multiselect_choices() { | |
get_choice_number() { | |
local options_idx="${1}" | |
local choice_num=" " | |
local count=0 | |
for i in ${choices[@]}; do | |
((count++)) | |
if [[ "${i}" = "${options_idx}" ]]; then | |
choice_num="*${count}" | |
break | |
fi | |
done | |
echo "${choice_num}" | |
} | |
menu() { | |
for i in ${!options[@]}; do | |
printf "%s %3d) %s\n" "$(get_choice_number $i)" $((i+1)) "${options[i]}" | |
done | |
if [[ "$msg" ]]; then | |
echo "$msg" | |
fi | |
} | |
prompt="Select boxes to ${action}, hit ENTER when all are selected: " | |
while menu && read -rp "$prompt" num && [[ "$num" ]]; do | |
[[ "$num" != *[![:digit:]]* ]] && | |
(( num > 0 && num <= ${#options[@]} )) || | |
{ msg="Invalid option: $num"; continue; } | |
((num--)); msg="" | |
rebuild_choices ${num} | |
done | |
} | |
build_select_options() { | |
for box_dir in $(_box_list); do | |
options+=("${box_dir}") | |
done | |
} | |
build_final_choices() { | |
for i in ${choices[@]}; do | |
final_choices+=("${options[${i}]}") | |
done | |
} | |
build_select_options | |
get_multiselect_choices | |
build_final_choices | |
} | |
_get_selected_boxes() { | |
local action="${1}" | |
local -n arr=$2 | |
if [ "${multiselect}" = "1" ]; then | |
multiselect arr "${action}" | |
else | |
PS3="Select box to ${action}: " | |
select box_dir in `_box_list`; do | |
arr=("${box_dir}") | |
break | |
done | |
fi | |
} | |
up_box() { | |
local box_list | |
_get_selected_boxes "bring up" box_list | |
_check_valid_selection "${box_list[@]}" && _box_command up "${box_list[@]}" | |
} | |
halt_box() { | |
local box_list | |
_get_selected_boxes "halt" box_list | |
_check_valid_selection "${box_list[@]}" && _box_command halt "${box_list[@]}" | |
} | |
reload_box() { | |
local box_list | |
_get_selected_boxes "reload" box_list | |
_check_valid_selection "${box_list[@]}" && _box_command reload "${box_list[@]}" | |
} | |
delete_box() { | |
PS3="Select box to delete: " | |
select box_dir in `_box_list`; do | |
_check_valid_selection "${box_dir}" && _confirm_delete_box ${box_dir} | |
break | |
done | |
} | |
action= | |
multiselect= | |
while getopts ":hdulrmcp:k:s:" option; do | |
case ${option} in | |
h ) | |
usage | |
exit 0 | |
;; | |
d ) | |
action="delete_box" | |
;; | |
u ) | |
action="up_box" | |
;; | |
l ) | |
action="halt_box" | |
;; | |
r ) | |
action="reload_box" | |
;; | |
c ) | |
action="create_box" | |
;; | |
m ) | |
multiselect=1 | |
;; | |
p ) | |
vagrant_dir=${OPTARG} | |
;; | |
k ) | |
ssh_pubkey_path=${OPTARG} | |
;; | |
s ) | |
custom_script=${OPTARG} | |
;; | |
esac | |
done | |
shift $((${OPTIND} - 1)) | |
if [ "${action}" = "create_box" ]; then | |
if [ -n "${1}" ]; then | |
box_dir=${1} | |
shift 1 | |
if [ -n "${1}" ]; then | |
ssh_port=${1} | |
shift 1 | |
fi | |
fi | |
fi | |
if [ $# -gt 0 ]; then | |
usage_short | |
exit 1 | |
elif [ -z "${action}" ]; then | |
usage_short | |
exit 0 | |
else | |
CWD=`pwd` | |
${action} | |
cd ${CWD} | |
exit 0 | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment