Last active
November 4, 2024 11:52
-
-
Save Ralnoc/af222d6dfbffcc00fb619152ada0fdc7 to your computer and use it in GitHub Desktop.
Take QCOW2 disk image and build a VM Template for Proxmox
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
#!/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}" |
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
#!/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