|
#!/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 |