Skip to content

Instantly share code, notes, and snippets.

@thehunmonkgroup
Last active May 6, 2026 00:49
Show Gist options
  • Select an option

  • Save thehunmonkgroup/1efccb1aca8ed023ba0e to your computer and use it in GitHub Desktop.

Select an option

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
#!/usr/bin/env bash
# Arguments: $1 = Host VirtualBox version, $2 = ISO download directory
HOST_VERSION="$1"
ISO_DIR="$2"
ISO_NAME="VBoxGuestAdditions_${HOST_VERSION}.iso"
ISO_PATH="${ISO_DIR}/${ISO_NAME}"
VBOXADD_CTL="$(command -v rcvboxadd 2>/dev/null || true)"
if [ -z "$VBOXADD_CTL" ] && [ -x /sbin/rcvboxadd ]; then
VBOXADD_CTL="/sbin/rcvboxadd"
fi
# Ensure the ISO directory exists
mkdir -p "$ISO_DIR"
# Extract the guest's VirtualBox Guest Additions version
GUEST_VERSION=$(VBoxControl --version 2>/dev/null | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+).*$/\1/')
# Compare versions
if [ "$HOST_VERSION" != "$GUEST_VERSION" ]; then
echo "Updating VirtualBox Guest Additions from $GUEST_VERSION to $HOST_VERSION"
mapfile -t EXISTING_VBOXSF_MOUNTS < <(awk '$3 == "vboxsf" {print $2}' /proc/mounts)
# Download the ISO if it doesn't exist
if [ ! -f "$ISO_PATH" ]; then
echo "Downloading VBoxGuestAdditions ISO to $ISO_PATH"
wget -q "https://download.virtualbox.org/virtualbox/${HOST_VERSION}/${ISO_NAME}" -O "$ISO_PATH"
else
echo "Using cached ISO at $ISO_PATH"
fi
# Mount and install
sudo mount -o loop "$ISO_PATH" /mnt
sudo sh /mnt/VBoxLinuxAdditions.run --nox11
sudo umount /mnt
if [ -z "$VBOXADD_CTL" ]; then
echo "ERROR: could not find rcvboxadd after updating VirtualBox Guest Additions." >&2
exit 1
fi
echo "Reloading VirtualBox Guest Additions services..."
sudo "$VBOXADD_CTL" reload
echo "Remounting VirtualBox shared folders from /etc/fstab..."
sudo mount -a -t vboxsf
missing_mounts=()
for mountpoint in "${EXISTING_VBOXSF_MOUNTS[@]}"; do
if ! awk -v mp="$mountpoint" '$2 == mp && $3 == "vboxsf" {found=1} END {exit !found}' /proc/mounts; then
missing_mounts+=("$mountpoint")
fi
done
if [ "${#missing_mounts[@]}" -gt 0 ]; then
echo "ERROR: VirtualBox Guest Additions updated, but the following shared folders were not restored:" >&2
printf ' %s\n' "${missing_mounts[@]}" >&2
echo "Run 'vagrant reload' to restore shared folders before continuing." >&2
exit 1
fi
echo "VirtualBox Guest Additions updated successfully."
else
echo "VirtualBox Guest Additions are up to date."
fi
#!/bin/bash
# Spins up a quick Vagrant box. Run without arguments for help.
_quick_vagrant_script_dir() {
local script_path="${BASH_SOURCE[0]}"
if [[ "${script_path}" != */* ]]; then
script_path="$(command -v -- "${script_path}")"
fi
if command -v readlink >/dev/null 2>&1; then
local resolved_script_path
resolved_script_path="$(readlink -f -- "${script_path}" 2>/dev/null)"
if [ -n "${resolved_script_path}" ]; then
script_path="${resolved_script_path}"
fi
fi
cd -- "$(dirname -- "${script_path}")" && pwd -P
}
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"
quick_vagrant_script_dir="$(_quick_vagrant_script_dir)"
vbguest_script_path="${quick_vagrant_script_dir}/install-vbguest.sh"
vbguest_host_iso_dir="~/Downloads"
vbguest_guest_iso_dir="/tmp/shared-downloads"
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.
Environment variables:
QUICK_VAGRANT_VBGUEST_SCRIPT_PATH: Path to install-vbguest.sh. Default is
'${vbguest_script_path}'.
QUICK_VAGRANT_VBGUEST_HOST_ISO_DIR: Host directory used to cache the
VirtualBox Guest Additions ISO. Default is '${vbguest_host_iso_dir}'.
QUICK_VAGRANT_VBGUEST_GUEST_ISO_DIR: Guest directory where the ISO cache is
mounted. Default is '${vbguest_guest_iso_dir}'.
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.
"
}
install_guest_kernel_and_build_packages() {
echo "Installing current guest kernel, headers, and build packages..."
if guest_has_command apt-get; then
install_guest_debian_kernel_and_build_packages
elif guest_has_command dnf; then
install_guest_rhel_kernel_and_build_packages dnf
elif guest_has_command yum; then
install_guest_rhel_kernel_and_build_packages yum
else
echo "No supported package manager found for kernel/header preparation."
fi
}
guest_has_command() {
local command_name=${1}
vagrant ssh -- "command -v ${command_name} >/dev/null 2>&1"
}
install_guest_debian_kernel_and_build_packages() {
local base_packages="build-essential dkms wget ca-certificates"
vagrant ssh -- "sudo apt-get -q update"
if vagrant ssh -- "apt-cache show linux-image-amd64 >/dev/null 2>&1"; then
vagrant ssh -- "sudo env DEBIAN_FRONTEND=noninteractive apt-get -q -y install ${base_packages} linux-image-amd64 linux-headers-amd64"
else
vagrant ssh -- "sudo env DEBIAN_FRONTEND=noninteractive apt-get -q -y install ${base_packages} linux-image-generic linux-headers-generic"
fi
}
install_guest_rhel_kernel_and_build_packages() {
local package_manager=${1}
local base_packages="gcc make perl bzip2 tar wget ca-certificates"
vagrant ssh -- "sudo ${package_manager} -y update 'kernel*'"
vagrant ssh -- "sudo ${package_manager} -y install ${base_packages} kernel-devel kernel-headers"
}
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
if [ -z "${box}" ]; then
echo "ERROR: Invalid selection: ${REPLY}"
continue
fi
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"
# 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"
# Some box packagers disable /vagrant, so declare it here specifically.
config.vm.synced_folder '.', '/vagrant',
type: 'virtualbox',
disabled: false,
owner: 'vagrant',
group: 'vagrant'
# VirtualBox VM tweaks.
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
# Guest additions management.
host_version = \`VBoxManage --version\`.match(/^(\d+\.\d+\.\d+)/)[1]
script_path = File.expand_path(ENV.fetch('QUICK_VAGRANT_VBGUEST_SCRIPT_PATH', '${vbguest_script_path}'))
host_iso_dir = File.expand_path(ENV.fetch('QUICK_VAGRANT_VBGUEST_HOST_ISO_DIR', '${vbguest_host_iso_dir}'))
guest_iso_dir = ENV.fetch('QUICK_VAGRANT_VBGUEST_GUEST_ISO_DIR', '${vbguest_guest_iso_dir}')
config.vm.synced_folder host_iso_dir, guest_iso_dir, owner: 'root', group: 'root'
config.vm.provision :shell,
run: 'always',
reset: true,
path: script_path,
args: [host_version, guest_iso_dir]
end
# vi: ft=ruby
EOF
vagrant up --no-provision || exit 1
install_guest_kernel_and_build_packages || exit 1
echo "Restarting server before provisioning..."
vagrant halt || exit 1
vagrant up --provision
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"
rm -f ${full_path}/Vagrantfile.bak
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 -- "if command -v dnf >/dev/null 2>&1; then sudo dnf -y install rsync vim-enhanced; elif command -v yum >/dev/null 2>&1; then sudo yum -y install rsync vim-enhanced; fi"
vagrant ssh -- "test -f /usr/bin/apt-get && sudo env DEBIAN_FRONTEND=noninteractive 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 -p /root/.ssh && sudo chmod 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