Skip to content

Instantly share code, notes, and snippets.

@Ralnoc
Last active November 4, 2024 11:52
Show Gist options
  • Save Ralnoc/af222d6dfbffcc00fb619152ada0fdc7 to your computer and use it in GitHub Desktop.
Save Ralnoc/af222d6dfbffcc00fb619152ada0fdc7 to your computer and use it in GitHub Desktop.
Take QCOW2 disk image and build a VM Template for Proxmox
#!/usr/bin/env bash
RED='\033[0;31m'
LIGHTRED='\033[1;31m'
GREEN='\033[0;32m'
LIGHTGREEN='\033[1;32m'
BROWN='\033[0;33m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
LIGHTBLUE='\033[1;34m'
CYAN='\033[0;36m'
LIGHTCYAN='\033[1;36m'
PURPLE='\033[0;35m'
LIGHTPURPLE='\033[1;35m'
DARKGRAY='\033[1;30m'
LIGHTGRAY='\033[0;37m'
WHITE='\033[1;37m'
NC='\033[0m'
set -eu
trap 'echo "$NAME: Failed at line $LINENO" >&2' ERR
NAME=${0##*/}
print_help() {
MSG=${1:-x}
if [[ ! "$MSG" == "x" ]]; then
printf "${RED}ERROR: %s${NC}\n" "$MSG" >&2
fi
printf "${WHITE}Usage: $NAME [options] <VMID> <HOST_NAME> <QCOW2_FILE> <SSH_PUB_KEY_FILE>${NC}\n" >&2
exit 1
}
getopt -T &>/dev/null && rc=$? || rc=$?
if ((rc != 4))
then
echo "This script requires gnu getopt" >&2
exit 1
fi
opts=$(getopt --name "$NAME" --options hs:d --longoptions help,ssh-pub-key:,bg-user,nameserver,search-domain,dryrun -- "$@") || print_help
eval set -- "$opts"
declare ENABLEDRYRUN=false SSH_PUB_KEY_FILE="${HOME}/.ssh/proxmox-vms-ssh.pub" DRYRUN=''
declare BG_USER="breakglass-user" NAMESERVER="192.168.1.10" SEARCH_DOMAIN="example.com"
while (($#))
do
case $1 in
-h|--help) print_help;;
-s|--ssh-pub-key) SSH_PUB_KEY_FILE=$2; shift;;
-b|--bg-user) BG_USER=$2; shift;;
-n|--nameserver) NAMESERVER=$2; shift;;
-e|--search-domain) SEARCH_DOMAIN=$2; shift;;
-d|--dryrun) ENABLEDRYRUN=true;;
--) shift; break;;
# Without "set -e" + ERR trap, replace "false" with an error message and exit.
*) false # Should not happen under normal conditions
esac
shift
done
if [[ "${ENABLEDRYRUN:-false}" == true ]]; then
printf '${RED}Executing as Dry Run. Commands not executed.${NC}\n'
DRYRUN='result_string '
fi
if (($# != 3))
then
print_help "Invalid number of arguments"
fi
read -r VMID HOST_NAME QCOW2_FILE <<< "$@"
error_handling_execute() {
COMMAND=$*
TOOL_REFERENCE=$(echo $1 | tr -dc '[:alnum:]\n\r' | tr '[:upper:]' '[:lower:]')
${COMMAND} 2>&1
RC=$?
if [ $RC -ne 0 ]; then
error_string "Error executing command ${COMMAND}"
exit $RC
else
success_string "${TOOL_REFERENCE} executed successfully "
return $RC
fi
}
warning_handling_execute() {
COMMAND=$*
TOOL_REFERENCE=$(echo $1 | tr -dc '[:alnum:]\n\r' | tr '[:upper:]' '[:lower:]')
${COMMAND} 2>&1
RC=$?
if [ $RC -ne 0 ]; then
warning_string "Warning executing command ${COMMAND}"
return $RC
else
success_string "${TOOL_REFERENCE} executed successfully "
return $RC
fi
}
error_string() {
MSG=$*
printf "${RED}${MSG}${NC}\n"
}
warning_string() {
MSG=$*
printf "${YELLOW}${MSG}${NC}\n"
}
success_string() {
MSG=$*
printf "${GREEN}${MSG}${NC}\n"
}
header_string() {
PADDING="================================================================================"
MSG=$*
printf "${PURPLE}==== ${MSG} %s${NC}\n" "${PADDING:${#MSG}+6}"
}
step_string() {
PADDING="================================================================================"
MSG=$*
printf "${BLUE}${MSG} %s${NC}\n" "${PADDING:${#MSG}}"
}
step_footer_string() {
printf "${BLUE}================================================================================${NC}\n"
}
action_string() {
MSG=$*
printf "${LIGHTBLUE}${MSG}${NC}\n"
}
result_string() {
MSG=$*
printf "${WHITE}${MSG}${NC}\n"
}
oneline() {
local ws
while IFS= read -r line; do
if (( ${#line} >= COLUMNS )); then
# Moving cursor back to the front of the line so user input doesn't force wrapping
printf '\r%s\r' "${line:0:$COLUMNS}"
else
ws=$(( COLUMNS - ${#line} ))
# by writing each line twice, we move the cursor back to position
# thus: LF, content, whitespace, LF, content
printf '\r%s%*s\r%s' "$line" "$ws" " " "$line"
fi
done
echo
}
header_string "Creating VM Template from QCOW2 file"
step_string "VM ${WHITE}${VMID}${BLUE} Initialization"
action_string "Creating VM for Template"
${DRYRUN} qm create "${VMID}" --name "${HOST_NAME}" --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
action_string "Importing QCOW2 file ${WHITE}${QCOW2_FILE}${LIGHTBLUE} for VM"
${DRYRUN} qm importdisk "${VMID}" "${QCOW2_FILE}" local-lvm |& oneline
step_string "Configuring VM Virtual Hardware Settings"
action_string "Attaching Imported QCOW Image to VM ${WHITE}${VMID}${LIGHTBLUE}"
${DRYRUN} qm set "${VMID}" --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-"${VMID}"-disk-0
action_string "Directing VM Console Output to Proxmox Console Interface"
${DRYRUN} qm set "${VMID}" --serial0 socket --vga serial0
action_string "Enabling QEMU Agent for VM"
${DRYRUN} qm set "${VMID}" --agent enabled=1
action_string "Attaching Cloudinit Drive to VM"
${DRYRUN} qm set "${VMID}" --ide2 local-lvm:cloudinit
step_string "Configuring Cloudinit Settings"
action_string "Configuring Drive Boot Order"
${DRYRUN} qm set "${VMID}" --boot order='net0;scsi0'
action_string "Settings Network Interface ${WHITE}eth0${LIGHTBLUE} to DHCP"
${DRYRUN} qm set "${VMID}" --ipconfig0 ip=dhcp
action_string "Assigning SSH Public Key to ${WHITE}${BG_USER}${LIGHTBLUE} Break Glass User"
${DRYRUN} qm set "${VMID}" --sshkeys "${SSH_PUB_KEY_FILE}"
action_string "Configuring Search Domain to ${WHITE}${SEARCH_DOMAIN}${LIGHTBLUE}"
${DRYRUN} qm set "${VMID}" --searchdomain ${SEARCH_DOMAIN}
action_string "Configuring DNS Server to ${WHITE}${NAMESERVER}${LIGHTBLUE}"
${DRYRUN} qm set "${VMID}" --nameserver ${NAMESERVER}
action_string "Configuring Cloud-init User ${WHITE}dmp-user${LIGHTBLUE}"
${DRYRUN} qm set "${VMID}" --ciuser ${BG_USER}""
action_string "Converting VM ${WHITE}${VMID}${LIGHTBLUE} to VM Template"
${DRYRUN} qm template "${VMID}"
#!/usr/bin/env bash
RED='\033[0;31m'
LIGHTRED='\033[1;31m'
GREEN='\033[0;32m'
LIGHTGREEN='\033[1;32m'
BROWN='\033[0;33m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
LIGHTBLUE='\033[1;34m'
CYAN='\033[0;36m'
LIGHTCYAN='\033[1;36m'
PURPLE='\033[0;35m'
LIGHTPURPLE='\033[1;35m'
DARKGRAY='\033[1;30m'
LIGHTGRAY='\033[0;37m'
WHITE='\033[1;37m'
NC='\033[0m'
set -eu
trap 'echo "$NAME: Failed at line $LINENO" >&2' ERR
NAME=${0##*/}
print_help() {
MSG=${1:-x}
if [[ ! "$MSG" == "x" ]]; then
echo "${RED}ERROR: %s${NC}" "$MSG" >&2
fi
printf "${WHITE}Usage: $NAME [options] <DISK_IMAGE> <VM_IMAGE_NAME> <VM_IMAGE_SIZE>${NC}\n" >&2
exit 1
}
getopt -T &>/dev/null && rc=$? || rc=$?
if ((rc != 4))
then
echo "This script requires gnu getopt" >&2
exit 1
fi
opts=$(getopt --name "$NAME" --options hd --longoptions help,dryrun -- "$@") || print_help
eval set -- "$opts"
declare ENABLEDRYRUN=false DRYRUN=''
while (($#))
do
case $1 in
-h|--help) print_help;;
-d|--dryrun) ENABLEDRYRUN=true;;
--) shift; break;;
# Without "set -e" + ERR trap, replace "false" with an error message and exit.
*) false # Should not happen under normal conditions
esac
shift
done
if [[ "${ENABLEDRYRUN}" == true ]]; then
printf '${YELLOW}Executing as Dry Run. Commands not executed.${NC}\n'
DRYRUN='result_string '
fi
if (($# != 3))
then
print_help "Invalid number of arguments"
fi
read -r DISTRO_NAME VM_IMAGE_NAME VM_IMAGE_SIZE <<< "$@"
VM_IMAGE_SIZE=$(echo ${VM_IMAGE_SIZE} | tr '[:lower:]' '[:upper:]')
error_handling_execute() {
COMMAND=$@
TOOL_REFERENCE=$(echo $1 | tr -dc '[:alnum:]\n\r' | tr '[:upper:]' '[:lower:]')
${COMMAND} 2>&1
RC=$?
if [ $RC -ne 0 ]; then
error_string "Error executing command ${COMMAND}"
exit $RC
else
success_string "${TOOL_REFERENCE} executed successfully "
return $RC
fi
}
warning_handling_execute() {
COMMAND=$@
TOOL_REFERENCE=$(echo $1 | tr -dc '[:alnum:]\n\r' | tr '[:upper:]' '[:lower:]')
${COMMAND} 2>&1
RC=$?
if [ $RC -ne 0 ]; then
warning_string "Warning executing command ${COMMAND}"
return $RC
else
success_string "${TOOL_REFERENCE} executed successfully "
return $RC
fi
}
error_string() {
MSG=$@
printf "${RED}${MSG}${NC}\n"
}
warning_string() {
MSG=$@
printf "${YELLOW}${MSG}${NC}\n"
}
success_string() {
MSG=$@
printf "${GREEN}${MSG}${NC}\n"
}
header_string() {
PADDING="================================================================================"
MSG=$@
printf "${PURPLE}==== ${MSG} %s${NC}\n" "${PADDING:${#MSG}+6}"
}
step_string() {
PADDING="================================================================================"
MSG=$@
printf "${BLUE}${MSG} %s${NC}\n" "${PADDING:${#MSG}}"
}
step_footer_string() {
printf "${BLUE}================================================================================${NC}\n"
}
action_string() {
MSG=$@
printf "${LIGHTBLUE}${MSG}${NC}\n"
}
result_string() {
MSG=$@
printf "${WHITE}${MSG}${NC}\n"
}
DISK_IMAGE="${DISTRO_NAME}-server-cloudimg-amd64.img"
DISK_IMAGE_URL="https://cloud-images.ubuntu.com/${DISTRO_NAME}/current/${DISK_IMAGE}"
VM_IMAGE_FILENAME="${VM_IMAGE_NAME}-${VM_IMAGE_SIZE}.qcow2"
MOTD=' ( * ( (
)\ ) ( ` )\ ) )\ ) ) (
(()/( )\))( (()/( (()/( ( /( ( )\ ) (
/(_)) ((_)()\ /(_)) /(_)))\()) ))\ (()/( )\ ( (
(_))_ (_()((_)(_)) (_)) (_))/ /((_) ((_))((_) )\ )\
| \ | \/ || _ \ / __|| |_ (_))( _| | (_) ((_)((_)
| |) || |\/| || _/ \__ \| _|| || |/ _` | | |/ _ \(_-<
|___/ |_| |_||_| |___/ \__| \_,_|\__,_| |_|\___//__/
-----------------------------------------------------------
NOTICE TO ALL USERS
This system is the property of DMP Studios. Unauthorized
access or use is strictly prohibited and may result in
disciplinary actions and/or criminal prosecution. All
activities are monitored and logged. By logging in, you
acknowledge and agree to comply with all company
policies and applicable laws.
-----------------------------------------------------------
'
MACHINE_ID_SYNC_SERVICE_FILENAME="machine-id-sync.service"
MACHINE_ID_SYNC_SERVICE_TARGET_PATH="/usr/lib/systemd/system/${MACHINE_ID_SYNC_SERVICE_FILENAME}"
MACHINE_ID_SYNC_SERVICE_CONTENT='[Unit]
Description=Regenerate machine-id if system-uuid does not match
Before=network-pre.target
Wants=network-pre.target
DefaultDependencies=no
Requires=local-fs.target
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/sbin/sync-machine-id.sh
RemainAfterExit=yes
[Install]
WantedBy=network.target
'
MACHINE_ID_SYNC='#!/usr/bin/env bash
# Machine ID Synchronizer
# Enforces sync between machine-id and system-uuid
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
UUID=$(dmidecode -s system-uuid | tr -d '-')
if grep -q "$UUID" /etc/machine-id; then
echo "UUID matches"
else
echo "UUID does not match. Recreating."
echo -n > /etc/machine-id && echo -n > /var/lib/dbus/machine-id && systemd-machine-id-setup
fi
'
header_string "Proxmox VM Cloud Disk Image Bootstrapping"
step_string "Environment setup"
action_string "Installing libguestfs-tools"
if dpkg -s libguestfs-tools &>/dev/null; then
success_string "libguestfs-tools is installed, Skipping installation."
else
action_string "Updating System Packages"
error_handling_execute apt update -y
action_string "Installing libguestfs-tools"
error_handling_execute apt install libguestfs-tools -y
fi
action_string "Obtaining Disk Image ${WHITE}${DISK_IMAGE}${BLUE}"
if [[ ! -f "${DISK_IMAGE}" ]]; then
action_string "Downloading ${WHITE}${DISK_IMAGE}${BLUE}"
${DRYRUN}wget -q --show-progress --progress=bar:force ${DISK_IMAGE_URL}
else
warning_string "Disk Image ${DISK_IMAGE} already exists. Skipping download."
fi
step_string "Initial Image Prep for ${WHITE}${VM_IMAGE_FILENAME}${BLUE}"
action_string "Copying ${WHITE}${DISK_IMAGE}${LIGHTBLUE} to ${WHITE}${VM_IMAGE_FILENAME}${LIGHTBLUE}"
if [ -f "${VM_IMAGE_FILENAME}" ]; then
error_string "VM Image ${VM_IMAGE_FILENAME} already exists. Exiting."
exit 1
fi
${DRYRUN}cp "$DISK_IMAGE" "$VM_IMAGE_FILENAME"
action_string "installing QEMU Guest Agent"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --install qemu-guest-agent
action_string "Installing Vim"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --install vim
action_string "Updating System Packages"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --update
action_string "Resizing Disk Image to ${WHITE}${VM_IMAGE_SIZE}${LIGHTBLUE}"
${DRYRUN}qemu-img resize "${VM_IMAGE_FILENAME}" "${VM_IMAGE_SIZE}"
step_string "Customizing Image Configuration"
action_string "Updating System MOTD"
${DRYRUN}echo "${MOTD}" > motd.txt
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --upload motd.txt:/etc/motd
${DRYRUN}rm -f motd.txt
${DRYRUN}echo "${MACHINE_ID_SYNC}" > machine-id.sh
action_string "Adding Machine ID Synchronizer Service"
${DRYRUN}echo "${MACHINE_ID_SYNC_SERVICE_CONTENT}" > "${MACHINE_ID_SYNC_SERVICE_FILENAME}"
action_string "Uploading machine-id-sync.sh to /sbin/sync-machine-id.sh"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --upload machine-id.sh:/sbin/sync-machine-id.sh
action_string "Setting 755 Permissions on /sbin/sync-machine-id.sh"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --chmod 755:/sbin/sync-machine-id.sh
action_string "Uploading machine-id-sync.service to ${MACHINE_ID_SYNC_SERVICE_FILENAME}"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --upload ${MACHINE_ID_SYNC_SERVICE_FILENAME}:${MACHINE_ID_SYNC_SERVICE_TARGET_PATH}
action_string "Enabling machine-id-sync.service"
${DRYRUN}virt-customize -a "${VM_IMAGE_FILENAME}" --run-command "systemctl enable machine-id-sync"
rm -f machine-id.sh
step_footer_string
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment