Last active
July 13, 2023 00:58
-
-
Save gabyx/40904a82bc9cba3b8e452865d74fd128 to your computer and use it in GitHub Desktop.
Unattended installation of any virtual machine with Virtual Box 6
This file contains hidden or 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 | |
# | |
# 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