Skip to content

Instantly share code, notes, and snippets.

@gabyx
Last active July 13, 2023 00:58
Show Gist options
  • Save gabyx/40904a82bc9cba3b8e452865d74fd128 to your computer and use it in GitHub Desktop.
Save gabyx/40904a82bc9cba3b8e452865d74fd128 to your computer and use it in GitHub Desktop.
Unattended installation of any virtual machine with Virtual Box 6
#!/bin/bash
#
# Unattendet installation of any virtual machine with
# Virtual Box 6.
# The script walks you through the needed options with
# default values.
#
# Usage:
# `installVirtualMachine.sh --force-delete \
# --post-script-url "https://server.com/script.sh"`
#
# Install and try to delete (with safety prompt) any existing
# virtual machine and use the post install script.
#
# Author: Gabriel Nützi gnuetzi(at)gmail(dot)com
trapWithArg() {
func="$1"
shift
for sig; do
trap "$func $sig" "$sig"
done
}
DO_CLEAN_UP="false"
trapWithArg cleanup EXIT INT TERM
die() {
logError "$1"
DO_CLEAN_UP="true"
exit 1
}
logError() {
echo "!! $1" >&2
}
# Check the output of a command
checkOutput() {
if [ $? -ne 0 ]; then
logError "Command failed: "
logError "$1"
return 1
fi
return 0
}
# Get input from the user.
getInput() {
read -p "$1 : " PROMPT_ANS </dev/tty
PROMPT_ANS=$(echo "$PROMPT_ANS" | xargs) # trim spaces
# Validate
if [ -n "$PROMPT_ANS" ]; then
if [ -n "$2" ]; then
if ! echo "$PROMPT_ANS" | grep -qE "$2"; then
logError "Answer is not validated with regex: '$2'"
return 1
fi
fi
ANSWER="$PROMPT_ANS"
fi
if [ -n "$ANSWER" ]; then
return 0
fi
return 1
}
isForceDelete() {
[ "$FORCE" = "true" ] || return 1
}
# Get input from the user.
getInputLoop() {
if [ "$3" != "--default" ]; then
unset ANSWER
fi
while true; do
if getInput "$1" "$2"; then
break
fi
done
}
# Get all os types
getOsTypes() {
LIST="$("$VBOXMANAGE" list ostypes 2>/dev/null | grep "^ID:" | sed -E "s@ID:\s+(.*)@\1@g" | sed -z 's/\n/|/g')"
LIST=$(echo "$LIST" | sed -E 's@\|$@@g') # remove last "|"
echo "$LIST"
}
# Create a a virtual machine an register it.
createVM() {
local VM="$1"
local DIR="$2"
local OS_TYPE="$3"
ANSWER="Linux_64"
getInput "Specify the os type of your virtual machine? [$ANSWER]" "$(getOsTypes)" ||
die "No valid virtual machine name given!"
VM_OS_TYPE="$ANSWER"
if isForceDelete; then
VM_CONFIG="$(find "$DIR/$VM" -name "*.vbox" | head)"
if [ -f "$VM_CONFIG" ]; then
getInput "Removing VM at '$VM_CONFIG' ? [N/y]" "(Nn|y)"
if [ "$ANSWER" = "y" ]; then
removeVM "$VM_CONFIG" || die "Could not remove VM at '$VM_CONFIG'"
fi
fi
unset VM_CONFIG # important for cleanup
fi
echo "Creating Virtual Machine ..."
OUT=$("$VBOXMANAGE" createvm \
--name "$VM" \
--basefolder "$DIR" \
--ostype "Linux_64" \
--register 2>&1)
#shellcheck disable=2181
checkOutput "$OUT" || return 1
VM_CONFIG="$(find "$DIR/$VM" -name "*.vbox" | head)"
OUT=$("$VBOXMANAGE" storagectl "$VM" \
--name "SATA Controller" \
--add sata \
--controller IntelAHCI)
checkOutput "$OUT" || return 1
echo "Adding IDE Controller ..."
OUT=$("$VBOXMANAGE" storagectl "$VM" --name "IDE Controller" --add ide 2>&1)
checkOutput "$OUT" || return 1
echo "Adding Settings ..."
OUT=$("$VBOXMANAGE" modifyvm "$VM" --ioapic on)
checkOutput "$OUT" || return 1
OUT=$("$VBOXMANAGE" modifyvm "$VM" --boot1 dvd --boot2 disk --boot3 none --boot4 none)
checkOutput "$OUT" || return 1
OUT=$("$VBOXMANAGE" modifyvm "$VM" --memory 4000 --vram 128)
checkOutput "$OUT" || return 1
return 0
}
# Create a virtual disk
createDisk() {
local VM="$1"
local DIR="$2"
local NAME="$3"
local SIZE="$4"
local FORMAT="$5"
local VARIANT="$6"
if [ -z "$NAME" ]; then
getInputLoop "Enter the file name of the virtual disk" ".*"
NAME="$ANSWER"
fi
if [ -z "$SIZE" ]; then
ANSWER="8000"
getInputLoop "Enter the file size in [mb] [$ANSWER]" "[[:digit:]]+" --default
SIZE="$ANSWER"
fi
if [ -z "$FORMAT" ]; then
getInputLoop "Enter the disk format [VDI|VMDK|VHD]" "(VDI|VMDK|VHD)"
FORMAT="$ANSWER"
SUFFIX=$(echo "$FORMAT" | tr '[:upper:]' '[:lower:]')
if ! echo "$NAME" | grep "\.$SUFFIX"; then
NAME="$NAME.$SUFFIX"
fi
fi
if [ -z "$VARIANT" ]; then
getInputLoop "Enter the variant of the disk [Standard|Fixed]" "(Standard|Fixed)"
VARIANT="$ANSWER"
fi
DISKPATH="$DIR/$VM/$NAME"
if [ -f "$DISKPATH" ]; then
logError "Disk already exists at '$DISKPATH'"
getInput "Do you want to delete it or do nothing and continue [N/y]" "(Nn|y)"
if [ "$ANSWER" = "y" ]; then
removeDisk "$DISKPATH" "--delete"
else
return 0
fi
fi
echo "Creating new virtual disk at '$DISKPATH'"
OUT=$("$VBOXMANAGE" createmedium disk \
--filename "$DISKPATH" \
--size "$SIZE" \
--format "$FORMAT" \
--variant "$VARIANT" 2>&1)
#shellcheck disable=2181
if ! checkOutput "$OUT"; then
logError "Maybe use :"
logError " '$VBOXMANAGE' closemedium '$DISKPATH' --delete"
logError "to delete a still registered disk"
return 1
fi
echo "Attaching disk to VM '$VM' ..."
attachDisk "$VM" "$DISKPATH" || die "Could not attach disk $DISK"
return 0
}
# Attach virtual disks
attachDisk() {
local VM="$1"
local DISK="$2"
OUT=$("$VBOXMANAGE" storageattach "$VM" \
--storagectl "SATA Controller" \
--port 0 \
--device 0 \
--type hdd \
--medium "$DISK")
checkOutput "$OUT" || return 1
return 0
}
# Create virtual disks
createDisks() {
local ASK="$1"
local VM="$2"
shift
if [ "$ASK" = "true" ]; then
while true; do
ANSWER="N"
getInput "Do you wish to add another volume? [N/y]" "(N|n|Y|y)"
case "$ANSWER" in
[Yy]*)
createDisk "$@" || die "Failed to add volume to '$VM'"
break
;;
[Nn]*) return 0 ;;
*) echo "Please answer yes or no." ;;
esac
done
else
createDisk "$@" || die "Failed to add volume to '$VM'"
fi
}
# Do the actual unattended install
doInstall() {
local VM="$1"
local VM_PATH="$2"
local CONFIG="$3"
local DIR=$(dirname "$CONFIG")
ANSWER="ubuntu20.04"
getInput "Which os should we install? [$ANSWER|custom]" "(ubuntu20.04|custom)"
if [ "$ANSWER" = "custom" ]; then
getInput "Enter the path to the iso image"
local ISO="$ANSWER"
ANSWER="Linux_64"
getInput "Specify the os type of your virtual machine? [$ANSWER]" "$(getOsTypes)" ||
die "No valid virtual machine name given!"
VM_OS_TYPE="$ANSWER"
elif [ "$ANSWER" = "ubuntu20.04" ]; then
ISO="$VM_PATH/ubuntu-20.04-desktop-amd64.iso"
if isForceDelete && [ -f "$ISO" ]; then
rm -rf "$ISO"
fi
if [ ! -f "$ISO" ]; then
if ! curl "https://releases.ubuntu.com/focal/ubuntu-20.04.1-desktop-amd64.iso" --output "$ISO"; then
logError "Could not download iso file!"
return 1
fi
fi
VM_OS_TYPE="Ubuntu_64"
fi
echo "Modify Os Type ..."
OUT=$("$VBOXMANAGE" modifyvm "$VM" --ostype "$VM_OS_TYPE")
checkOutput "$OUT" || return 1
echo "Adding ISO image '$ISO' ..."
OUT=$("$VBOXMANAGE" storageattach "$VM" \
--storagectl "IDE Controller" \
--port 0 \
--device 0 \
--type dvddrive \
--medium "$ISO" 2>&1)
checkOutput "$OUT" || return 1
ANSWER="default"
getInput "Enter the user account name of your new virtual machine [$ANSWER]"
USER_NAME="$ANSWER"
ANSWER="default"
getInput "Enter the user account password of your new virtual machine [$ANSWER]"
PASSWORD="$ANSWER"
ANSWER="$(echo "$VM" | tr '[:upper:]' '[:lower:]').local"
getInput "Enter the hostname of your new virtual machine [$ANSWER]"
HOSTNAME="$ANSWER"
POST_COMMAND=""
if [ -z "$POSTSCRIPT" ]; then
ANSWER=""
getInput "Enter post install script url [none]"
POSTSCRIPT="$ANSWER"
fi
if [ -n "$POSTSCRIPT" ]; then
if ! curl "$POSTSCRIPT" &>/dev/null; then
die "Post install script is not available at '$POSTSCRIPT'"
fi
#shellcheck disable=2089
POST_COMMAND="\"sudo su -c 'bash <(wget -q0- '$POSTSCRIPT')'\""
fi
echo "Starting unattended install [minimal] (config at '$DIR/unattended.config')..."
#shellcheck disable=2090,2086
"$VBOXMANAGE" unattended install "$VM" \
--iso="$ISO" \
--user="$USER_NAME" \
--full-user-name="$USER_NAME" \
--password="$PASSWORD" \
--hostname="$HOSTNAME" \
--install-additions \
--package-selection-adjustment=minimal \
--time-zone=UTC \
--post-install-command="$POST_COMMAND" 2>&1 1>"$DIR/unattended.config"
checkOutput "$OUT" || return 1
OUT=$("$VBOXMANAGE" startvm "$VM" 2>&1)
checkOutput "$OUT" || return 1
return 0
}
# Remove a virtual disk
removeDisk() {
echo "Removing disk at '$1' ... "
if ! "$VBOXMANAGE" closemedium disk "$1" $@ &>/dev/null; then
return 1
fi
return 0
}
# Remove a virtual machine
removeVM() {
local VM_CONFIG="$1"
if "$VBOXMANAGE" unregistervm "$VM_CONFIG" --delete &>/dev/null; then
return 0
fi
return 1
}
# Clean up virtual machine in case of failure
cleanup() {
#shellcheck disable=2181
local sig="$1"
if [ "$DO_CLEAN_UP" = "true" ] || [ "$sig" = "INT" ] || [ "$sig" = "TERM" ]; then
if [ -f "$VM_CONFIG" ]; then
echo "Cleanup ..."
removeVM "$VM_CONFIG" || logError "Could not remove vm '$VM_CONFIG' for clean up"
fi
fi
exit $?
}
parseCommandLine() {
FORCE="false"
for p in "$@"; do
if [ "$p" = "--force-delete" ]; then
FORCE="true"
elif [ "$p" = "--post-script-url" ]; then
true
elif [ "$prev" = "--post-script-url" ]; then
POSTSCRIPT="$p"
else
echo "! Unknown argument \`$p\`" >&2
return 1
fi
local prev="$p"
done
return 0
}
runInstall() {
VBOXMANAGE="VBoxManage.exe"
if ! command -v "$VBOXMANAGE" &>/dev/null; then
VBOXMANAGE="/c/Program Files/Oracle/VirtualBox/VBoxManage.exe"
fi
command -v "$VBOXMANAGE" &>/dev/null || die "No command $(VBoxManage.exe) found!"
ANSWER="MyVirtualMachine"
getInput "What is the name of your virtual machine? [$ANSWER]" ||
die "No valid virtual machine name given!"
VM="$ANSWER"
ANSWER="./"
getInput "Specify the path were this virtual machine will be created? [$ANSWER]" ||
die "No valid path given!"
VM_PATH="$ANSWER"
mkdir -p "$VM_PATH" || die "Could not create folder $VM_PATH!"
VM_PATH=$(cd "$VM_PATH" && pwd)
createVM "$VM" "$VM_PATH" "$VM_OS_TYPE" || die "Could not create virtual machine '$VM'"
echo "Creating Disks ..."
createDisks "false" "$VM" "$VM_PATH" "$VM.vdi" "" "VDI" "Standard" || die "Could not create disks for '$VM'"
createDisks "true" "$VM" "$VM_PATH" || die "Could not create disks for '$VM'"
doInstall "$VM" "$VM_PATH" "$VM_CONFIG" || die "Could not install '$VM'"
}
parseCommandLine "$@" || die "Wrong command line"
runInstall
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment