#! /bin/bash ## Copyright (c) Alex Genovese https://github.com/TuxVinyards # licence: GPL3 https://www.gnu.org/licenses ## Provides a set of qcow snapshot & msr tools to work with quickemu # https://github.com/quickemu-project/quickemu https://gitlab.com/qemu-project/qemu # Users should install 'quickemu' and may set up Virtual Machines as normal. ## Install by placing script in /usr/bin (ensure chmod +x) # Run by opening a terminal in the VM folder and typing: # quickemu-tools --vm "file.conf" ## Based on 'quickemu-mod' https://github.com/TuxVinyards/quickemu-mod # but, as having run time mods & hypervisor recipes removed, this 'tools' version # now disengages it from having to keep track of the main project ... # In theory, further disengagement could be achieved by routing straight 'qemu-img' # But as this should work with any version of the original 'quickemu' # the quickemu methods have been left, so any future add-ons might be possible. REVIEW ## Any snippets re-used from https://github.com/quickemu-project/quickemu # are used to make the two projects work together easily & are used mainly # for the benefit of the original project. # Original snippets may be subject the original MIT licence. # The 'tools' project, as separate script, is covered by GPL3 # even it it becomes adopted by the original project. # IF ANY 'MODDED' CODE BECOMES USED IN THE ORIGINAL QUICKEMU SCRIPTS, # OR ANY OTHER SIMILAR PROJECT, YOU SHOULD SHOW BOTH OF THE LICENCES # & SHOW CLEAR ATTRIBUTIONS TO THE CODE SECTIONS USED. QtoolsVersion="2023.03.28" ## API --vm "file.conf" [ --path "path/folder" ] # where path to be used if .conf file not in current folder / present working directory ## SETTINGS # General color & theming X_Shade="3" # Yellow 3 (recommended), Blue 4, Cyan 6 (brighter blue), Red 1 # https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit # File for default KVM behaviour for unhandled machine-specific registers. ( REVIEW ) # Edit here if your OS locates 'modprobe.d' differently. Default is "/etc/modprobe.d/kvm-quickemu.conf" KVM_MSR_ModProbeFile="/etc/modprobe.d/kvm-quickemu.conf" CurrentFolder="$(pwd)" ## Make sure shell is set during session to decimal separator of dot # LC_ALL=C changes too much, just set the numeric. # See locale setting discussion: https://unix.stackexchange.com/a/149129 # Also https://unix.stackexchange.com/questions/62316/why-is-there-no-euro-english-locale?rq=1 # & http://www.unicode.org/L2/L2001/01102-POSIX15897.htm export "LC_NUMERIC=C" export "LC_COLLATE=C" ## MOD Standard Quickemu checks for '< 4' which at 2022/3 now needs a bump. # Some ver 5 script is now present in the standard release too ... # More Version 5 style scripting should be used: # See http://mywiki.wooledge.org/BashGuide/Practices#Choose_Your_Shell if ((BASH_VERSINFO[0] < 5)); then echo "Sorry, you need Bash 5.0 or newer to run this script." exit 1 fi LAUNCHER="$(basename "$0")" if [[ ! $(type -p quickemu) ]]; then echo "ERROR! QuickEmu not found. Please install." exit 1 fi # TODO add 'guestfs-tools' & an interface for 'virt-resize' printColor () { tput setaf "$X_Shade" # shellcheck disable=SC2059 printf "$@" tput sgr0 } exit () { # trap to keep terminal open if started by mouse click -t secs printf "\n\n" if [[ ! $CLI ]] && [[ $1 ]]; then printColor " ERROR : [Enter] to quit or [h] to hold terminal open \n\n" read -rp " > " -t 30 ExitTrap fi [[ ! $CLI ]] && [[ $ExitTrap == "h" ]] && printf "\n\n Holding terminal open [Enter] to quit \n\n" && read -rp " > " tput cnorm command exit "$@" } function_find_kvm_msr_default_and_status () { # outputs boths vars 'KVM_MSR_DefaultConf' & 'KVM_MSR_status' with value Y or N # finds and flags if MSRS has a config conflict KVM_MSR_status="$(cat /sys/module/kvm/parameters/ignore_msrs)" [[ ! $KVM_MSR_ModProbeFile ]] && KVM_MSR_ModProbeFile="/etc/modprobe.d/kvm-quickemu.conf" KVM_MSR_DefaultConf="$(cat "$KVM_MSR_ModProbeFile" 2> /dev/null)" [[ "$KVM_MSR_DefaultConf" == *'=Y' ]] && KVM_MSR_default="Y" [[ "$KVM_MSR_DefaultConf" == *'=N' ]] || [[ ! -e "$KVM_MSR_ModProbeFile" ]] && KVM_MSR_default="N" if [[ $VM_InstanceName ]]; then if [[ "$VM_InstanceName" == *windows* ]] || [[ "$VM_InstanceName" == *macos* ]] ; then if [[ $KVM_MSR_status == "N" ]]; then KVM_MSR_Error=1 ; else KVM_MSR_Error= ; fi elif [[ "$VM_InstanceName" != *windows* ]] && [[ "$VM_InstanceName" != *macos* ]] ; then if [[ $KVM_MSR_status == "Y" ]]; then KVM_MSR_Error=1 ; else KVM_MSR_Error= ; fi else KVM_MSR_Error= fi fi } print_kvm_status () { function_find_kvm_msr_default_and_status if [[ $KVM_MSR_status == "Y" ]] ; then printf "\n\n KVM: /sys/module/kvm/parameters/ignore_msrs = Y" [[ $VM_InstanceName ]] && [[ $KVM_MSR_Error ]] && printColor " ERROR " printf "\n" else printf "\n\n KVM: /sys/module/kvm/parameters/ignore_msrs = N" [[ $VM_InstanceName ]] && [[ $KVM_MSR_Error ]] && printColor " ERROR " printf "\n" fi } toggle_msr_defaults () { # Modded & now reversible rewrite of original quickemu's function 'ignore_msrs_always' # https://www.linux-kvm.org/page/Category:Docs if [[ ! -d /etc/modprobe.d ]]; then printf "\n ERROR! /etc/modprobe.d was not found. \n\n See notes, it may be possible to manually create modprobe.d/kvm-quickemu.conf \n\n" else printColor "\n\n Configure default, boot-up, KVM behaviour " printf "for unhandled machine-specific registers" printf "\n\n Normal setting is N (don't ignore) but Windows and MacOS require Y (true) 'ignore' " function_find_kvm_msr_default_and_status printColor "\n\n Status: /sys/module/kvm/parameters/ignore_msrs = %s Current Default = %s" "$KVM_MSR_status" "$KVM_MSR_default" [[ ! $KVM_MSR_ModProbeFile ]] && KVM_MSR_ModProbeFile="/etc/modprobe.d/kvm-quickemu.conf" if [[ ! -e "$KVM_MSR_ModProbeFile" ]]; then printf "\n\n \'%s\' needs to be created " "$KVM_MSR_ModProbeFile" fi printf "\n\n [y] to set Y [n] to set N [b] to go back \n\n" read -rp " > " Set_MSR_defaults # set .conf file content & update initramfs in all kernels (y/n or none) if [[ $Set_MSR_defaults == "y" ]]; then printf "\n\n Updating 'initramfs' may take a moment or two ... \n\n" # As per Martin's solution in original quickemu, needs 'tee' to get this to work, # but route tee's stdout to null to tidy the screen echo "options kvm ignore_msrs=Y" | sudo tee "$KVM_MSR_ModProbeFile" 1> /dev/null sudo update-initramfs -k all -u elif [[ $Set_MSR_defaults == "n" ]]; then printf "\n\n Updating 'initramfs' may take a moment or two ... \n\n" echo "options kvm ignore_msrs=N" | sudo tee "$KVM_MSR_ModProbeFile" 1> /dev/null sudo update-initramfs -k all -u fi fi [[ $CLI ]] && exit 1 } show_kvm_sudo_security_note () { printColor "\n QuickEmu-Tools require 'sudo' permissions to echo true or false to 'ignore_msrs'" printf "\n\n This allows you to create a temporary MSRS status that may be changed at any time," printf "\n\n allowing you to match the selected guest VM that you want to run." printColor "\n\n\n If you have concerns about this script, or about giving elevated permissions, " printf "\n\n then the script should be checked or you should issue these commands manually:" printf "\n\n Open a side terminal, use shift-crtl-c to copy the displayed command & shift-crtl-v to paste it. " printf "\n\n Elevated permissions will then exist only in the side terminal & cease once it is closed. " printf "\n\n Return to q-tools & select 'leave as'. Q-Tools will re-read msrs settings & auto-update. " printColor "\n\n\n If you mainly use Windows or Mac VM's then a file '.../modprobe.d/kvm-quickemu.conf' " printf "\n\n can be created to modify the load up settings. Quickemu-Tools has a new built in function" printf "\n\n that can set this up & also allows future adjustments may be made." printf "\n\n Or it may be carried out manually... See settings, script & further notes for details." printColor "\n\n\n Status: /sys/module/kvm/parameters/ignore_msrs = %s Current Default = %s" "$KVM_MSR_status" "$KVM_MSR_default" printf "\n\n Windows or MacOS should be set to 'Y' " printf "\n" } select_msr_config () { # MSR_offer normally present if MSRS/OS conflict previously detected, # however, presume selector is being used to change current status REVIEW print_kvm_status if [[ $KVM_MSR_status == "Y" ]]; then MSR_offer="N" ; else MSR_offer="Y" ; fi KVM_MSR_selector= [[ $KVM_MSR_selector_LoadHelp ]] && show_kvm_sudo_security_note while true ; do if [[ $MSR_offer == "Y" ]]; then printf "\n\n Set Y : echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs ? " printColor "\n\n [y] to set Y " printf " [enter] leave as N" else printf "\n\n Set N : echo 0 | sudo tee /sys/module/kvm/parameters/ignore_msrs ? " printColor "\n\n [n] to set N " printf " [enter] leave as Y" fi printf " [d] to set the boot defaults" [[ $KVM_MSR_selector != "h" ]] || [[ $KVM_MSR_selector_LoadHelp ]] && printf " [h] see help " printf "\n\n" read -rp " > " KVM_MSR_selector printf "\n" [[ $KVM_MSR_selector == "h" ]] && show_kvm_sudo_security_note [[ ! $KVM_MSR_selector ]] && break [[ $KVM_MSR_selector == "y" && $MSR_offer == "N" ]] || [[ $KVM_MSR_selector == "n" && $MSR_offer == "Y" ]] && break if [[ $KVM_MSR_selector == "y" ]]|| [[ $KVM_MSR_selector == "n" ]]; then # As per Martin's solution in original quickemu, needs 'tee' to get this to work, # but route tee's stdout to null to tidy the screen [[ $KVM_MSR_selector == "y" ]] && echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs 1> /dev/null [[ $KVM_MSR_selector == "n" ]] && echo 0 | sudo tee /sys/module/kvm/parameters/ignore_msrs 1> /dev/null print_kvm_status printColor "\n\n [enter] to return \n\n" read -rp " > " break fi if [[ $KVM_MSR_selector == "d" ]]; then toggle_msr_defaults if [[ $Set_MSR_defaults == "b" ]]; then Set_MSR_defaults= print_kvm_status printColor "\n\n Make TEMPORARY setting adjustments to MSRS ?" else function_find_kvm_msr_default_and_status break fi fi done KVM_MSR_selector= KVM_MSR_selector_LoadHelp= } msrs_conflict_check_resolver() { # Do a check ... function_find_kvm_msr_default_and_status # Display & Offer config settings if MSRS/OS CONFLICT exists if [[ $KVM_MSR_status == "N" ]] ; then # usual system default = N if [[ "$VM_InstanceName" == *windows* ]] || [[ "$VM_InstanceName" == *macos* ]] ; then printColor "\n\n Selected: %s " "$VM_InstanceName" printf " 'ignore_msrs' is recommended for Windows and Mac" #printf "\n\n Status: /sys/module/kvm/parameters/ignore_msrs = N Default = %s" "$KVM_MSR_default" MSR_offer="Y" select_msr_config function_find_kvm_msr_default_and_status if [[ $KVM_MSR_status == "N" ]]; then KVM_MSR_Error=1 ; else KVM_MSR_Error= ; fi fi else # Status = Y & which is only recommended for Windows & Mac if [[ "$VM_InstanceName" != *windows* ]] && [[ "$VM_InstanceName" != *macos* ]] ; then printColor "\n\n Selected: %s " "$VM_InstanceName" printf " 'ignore_msrs' is only recommended for Windows and Mac" #printf "\n\n Status: /sys/module/kvm/parameters/ignore_msrs = Y Default = %s" "$KVM_MSR_default" MSR_offer="N" select_msr_config function_find_kvm_msr_default_and_status if [[ $KVM_MSR_status == "Y" ]]; then KVM_MSR_Error=1 ; else KVM_MSR_Error= ; fi fi fi } function_conf_error () { printf "\n\n ERROR Quickemu-Tools Settings, VM folder & conf file(s)" if [[ $1 ]] ; then printf "\n\n Please check %s settings, location & content ... \n\n" "$1" else printf "\n\n Please check the settings and re-run this script ... \n\n" ; fi printColor "\n\n [Enter] for help [q] to quit \n\n" read -rp " > " [[ $REPLY == "q" ]] && printf "\n\n" && command exit show_quickemu_tools_help command exit } function_snapshot_list() { printf "\n\n" quickemu -vm "$VM_Conf_File" --snapshot info } function show_quickemu_tools_help { printColor "\n\n QuickEmu-Tools version %s " "$QtoolsVersion" printColor "\n\n Easy snapshot & MSRS tools for the QuickEmu Project" printf "\n\n Manage multiple snapshots. Recover Disk Space. " printf "\n\n Toggle boot-up & temporary MSRS's " printf "\n\n\n For code contributions, add-ons, info & updates see:" printf "\n\n https://github.com/TuxVinyards/" printf "\n\n\n From a terminal: " printColor "%s --vm \"vm-name.conf\" [ --path \"path/folder\" ] " "$LAUNCHER" printf "\n\n Add path if working from outside the VM's .conf folder" printf "\n\n [enter] to return \n\n" read -rp " > " } show_qmod_title() { printColor "\n\n Quickemu Tools - Version %s" "$QtoolsVersion" printf "\n\n A menu interfaced tool set for the quickemu project ..... \n\n" } function_show_main_menu_header () { printf "\033c" show_qmod_title printColor " %s " "$VM_InstanceName" printf " @ %s" "$VM_Conf_Dir" } ## API READ #################################################################################################### # https://unix.stackexchange.com/questions/220330/hide-and-unhide-cursor-with-tput tput civis if [[ ! $1 ]]; then show_quickemu_tools_help echo command exit 1 else ## API --vm "file.conf" [ --path "path/folder" ] # where path to be used if .conf file not in current folder / present working directory while [[ $# -gt 0 ]]; do case "$1" in -vm|--vm) VM_Conf_File="$2" shift shift ;; --path|-path) VM_Conf_Dir="$2" shift shift ;; esac done [[ ! $VM_Conf_Dir ]] && VM_Conf_Dir="$CurrentFolder" [[ ! $VM_Conf_File ]] && function_conf_error "Qtools COMMAND LINE Instruction," [[ ! -e "$VM_Conf_Dir/$VM_Conf_File" ]] && function_conf_error "Qtools COMMAND LINE Instruction," ## Check file/folder exists [[ ! -d "$VM_Conf_Dir" ]] && function_conf_error "folder" # change directory to where the VM is if [[ $CurrentFolder != "$VM_Conf_Dir" ]]; then ! cd "$VM_Conf_Dir" && printColor "\n\n ERROR .conf folder switching \n\n" && exit 1 fi [[ ! -e "$VM_Conf_File" ]] && function_conf_error ".conf file" # Quickemu sets the same name to the .conf file and to the main folder VM_InstanceName="${VM_Conf_File/.conf}" # VM_QCOW_Dir="$VM_Conf_Dir/$VM_InstanceName" # check that the dir contains the right files && grep .conf for right content [[ ! $(ls "$VM_InstanceName"/*.qcow2 2> /dev/null) ]] && function_conf_error "folder" ! grep -q 'guest_os=' "$VM_Conf_File" && function_conf_error ".conf file" ## Check KVM parameter settings & advise according to guest OS KVM_MSR_selector= function_find_kvm_msr_default_and_status #msrs_conflict_check_resolver fi printf "\033c" show_qmod_title MultiInstanceError="$(pgrep -c 'quickemu-mod')" if [[ $MultiInstanceError -gt 1 ]]; then printColor "\n\n ERROR more than one instance of q-tools is running \n\n" read -rp " Close the other instances, then press [enter] to continue > " fi check_instance_runtime() { # https://www.qemu.org/docs/master/tools/qemu-img.html InstancePID="$(pgrep "$VM_InstanceName")" if [[ $InstancePID ]]; then printColor "\n\n WARNING snapshots operations should NOT be carried out when VM running \n\n" read -rp " Close down the VM, then press [enter] to continue > " fi } check_instance_runtime # MAIN MENU (select VM then choose actions to do) while true ; do MainMenuChoice= SnapTitle= SnapNumber= SnapName= function_show_main_menu_header if [[ ! $MainMenuChoice ]]; then print_kvm_status printf "\n\n [m] toggle msrs " printf "\n\n\n [sl] list [sc] create [sd] delete [sa] apply snapshots " printf "\n\n\n [h] show help & info " printf "\n\n [q] quit " printf "\n\n\n" read -rp " > " MainMenuChoice fi # ACTIONS: if [[ $MainMenuChoice == "h" ]] ; then show_quickemu_tools_help elif [[ $MainMenuChoice == "m" ]] ; then #KVM_MSR_selector_LoadHelp=1 select_msr_config #msrs_conflict_check_resolver elif [[ $MainMenuChoice == "q" ]] ; then printf "\n\n" MainMenuChoice= break exit elif [[ $MainMenuChoice == "sl" ]] ; then check_instance_runtime function_snapshot_list printf "\n\n [enter] to return to menu \n\n " read -rp " > " elif [[ $MainMenuChoice == "sc" ]] ; then check_instance_runtime function_snapshot_list printColor "\n\n Give [title] or [enter] for date.time [b] back to menu " SnapTitle= printf "\n\n" read -rp " > " SnapTitle printf "\n\n" [[ ! $SnapTitle ]] && SnapTitle="$(date +%b%d.%R)" [[ $SnapTitle != "b" ]] && quickemu -vm "$VM_Conf_File" --snapshot create "$SnapTitle" printf "\n\n [enter] to return to menu \n\n " read -rp " > " elif [[ $MainMenuChoice == "sd" ]] ; then printColor "\n\n Quickemu-Tools Snapshot Deletion:" check_instance_runtime function_snapshot_list # Create range-selectable array SnapListString="$(function_snapshot_list | grep '[0-9][0-9]:')" mapfile -t SnapListArrRaw <<< "$SnapListString" i=0 printColor "\n\n ID Array Name \n\n" while [[ "${SnapListArrRaw[$i]}" ]]; do IFS=' ' read -ra SnapListArrSeparated <<< "${SnapListArrRaw[$i]}" printf "%2d %2d %s \n" "${SnapListArrSeparated[0]}" "$i" "${SnapListArrSeparated[1]}" ((i+=1)) done SnapListArrTotal=$((i-1)) printColor "\n\n Give ARRAY number 0 to %s of snapshot or start of snapshot range to delete" "$SnapListArrTotal" printf "\n\n [enter] to return to main menu " SnapName= SnapDeleteStart= SnapDeleteEnd= SnapDeleteConfirm= printf "\n\n" read -rp " > " SnapDeleteStart if [[ $SnapDeleteStart ]]; then printColor "\n\n [enter] for individual snapshot or ARRAY [number] for end of range (inclusive) \n\n" read -rp " > " SnapDeleteEnd if [[ $SnapDeleteEnd ]]; then printf "\n Array Range = %s to %s " "$SnapDeleteStart" "$SnapDeleteEnd" else printf "\n Delete = Array entry %s " "$SnapDeleteStart" SnapDeleteEnd="$SnapDeleteStart" fi printColor "\n\n [enter] to continue [b] back to main menu \n\n" read -rp " > " SnapDeleteConfirm if [[ $SnapDeleteConfirm == "b" ]]; then printf "\n\n Deletion schedule has been CANCELLED" else SnapDeleteRangeCounter=$SnapDeleteStart while [[ $SnapDeleteRangeCounter -le $SnapDeleteEnd ]]; do IFS=' ' read -ra SnapListArrSeparated <<< "${SnapListArrRaw[$SnapDeleteRangeCounter]}" SnapName="${SnapListArrSeparated[1]}" if [[ ! $SnapName ]]; then printColor "\n\n ERROR with SnapShot Array List \n\n" exit 1 else printColor "\n\n Deleting SnapShot %2d %2d %s \n\n" "${SnapListArrSeparated[0]}" "$SnapDeleteRangeCounter" "${SnapListArrSeparated[1]}" quickemu -vm "$VM_Conf_File" --snapshot delete "$SnapName" fi ((SnapDeleteRangeCounter+=1)) done fi printf "\n\n [enter] to return to menu \n\n " read -rp " > " fi elif [[ $MainMenuChoice == "sa" ]] ; then check_instance_runtime function_snapshot_list printColor "\n\n Give number of snapshot to use [enter] to return to menu " SnapNumber= printf "\n\n" read -rp " > " SnapNumber printf "\n\n" if [[ $SnapNumber ]]; then quickemu -vm "$VM_Conf_File" --snapshot apply "$SnapNumber" printf "\n\n May take a moment .... \n\n" printColor "\n\n Snapshot %s has been applied. \n\n" "$SnapNumber " fi fi done # vim:tabstop=2:shiftwidth=2:expandtab ##