Today a friend remarked on his interest to learn Vagrant , and lack of time to do so. It occurred to me that the script I’ve been building to quickly knock up and down Vagrant VMs could be a handy aid along the shortest path for developers to begin their journey into this amazing toolset.
So without further ado, the shortest path I know of for a busy dev to start playing with Vagrant:
Install VirtualBox and Vagrant
…or for Homebrew Cask users, the even easier brew cask install virtualbox vagrant
…
Run the following from CLI:
vagrant box add bento/centos-7.1 --provider virtualbox
vagrant box add bento/debian-8.2 --provider virtualbox
…or pick some other box you like here .
Download the script below, stick it in your PATH, make it executable
Execute without args for the well-written help
…or run quick-vagrant.sh -c
to spin up your first box…
Enjoy :)
I also made the script a bit like a cheat sheet for Vagrant commands – if you search it for ‘vagrant’, you’ll see the first time any command is used, there’s a comment describing what it does. Or, here’s a more neatly formatted cheat sheet .
If you find this helpful getting up to speed, please pass it along to your busy developer friends! If you’d like to suggest improvements to the script, feel free to contact me .
#!/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