#!/bin/bash #shellcheck disable=1090 disable=2012 #------------------------------------------------------------------------# # Copyright 2021, Jesse Gardner # #------------------------------------------------------------------------# # This file is part of qq2clone. # # # # qq2clone is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 2 of the License, or # # (at your option) any later version. # # # # qq2clone is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with qq2clone. If not, see . # #------------------------------------------------------------------------# #--------------------# #@@@@@@@@@@@@@@@@@@@@# #---LITERAL VALUES---# #@@@@@@@@@@@@@@@@@@@@# #--------------------# E_permission=10 # No permission for access or file does not exist E_depends=11 # Lacking required software E_args=12 # Bad command line arguments or arguments specify illegal # action E_template=13 # Problem with a template E_extcom=14 # An external command failed E_xml=15 # Malformed XML or issue processing XML E_libvirt=16 # Invokation of a libvirt tool was unsuccesful E_timeout=17 # Timeout was exceeded before spice connection to VM # was established E_file=18 # Expected file does not exist or is of wrong type/format E_unexpected=19 # Probably a bug in qq2clone # lv_api_do prints one of these when started CONN_BAD="# No Connection" CONN_GOOD="# Connected" # lv_api_do prints one of these immediately following a line of input BAD_REQ="# Bad Request" FATAL="# Fatal error" GOOD="# Making API Request" NOMATCH="# No matching domain" #---------------------------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---NAMED PIPE FOR PASSING DATA BETWEEN FUNCTIONS---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---------------------------------------------------# #==============================================================================# check_pipe () # DESCRIPTION: See if pipe is open # INPUT: None # OUTPUT: Return value # PARAMETERS: None #==============================================================================# { local type type="$(file -bL /proc/self/fd/3)" if [[ ! "$type" =~ fifo ]]; then return 1 fi return 0 } #==============================================================================# open_pipe () # DESCRIPTION: Open a named pipe in read/write mode on fd3 # INPUT: None # OUTPUT: None # PARAMETERS: None #==============================================================================# { check_pipe && return local fifo_path fifo_path=$(mktemp -d) || temp_error #shellcheck disable=2064 trap "exec 3>&-; exec 3<&-;rm -rf $fifo_path" EXIT mkfifo "$fifo_path/fifo" || fifo_error exec 3<>"$fifo_path/fifo" return 0 } #==============================================================================# read_pipe () # DESCRIPTION: Flushes the contents of the named pipe to stdout, # nonblocking # INPUT: None # OUTPUT: Contents of named pipe on fd3 # PARAMETERS: $1: (Optional) If 1, read data out but also write it back # into the pipe #==============================================================================# { # Note: This implementation allows for things like this to work: # tr "a" "b" < <(read_pipe) | write_pipe 1 echo "EOF" >&3 local line match while IFS= read -r line <&3; do # write_pipe puts a + at the start of every line if [[ "$line" =~ ^\+(.*)$ ]]; then match="${BASH_REMATCH[1]}" echo "$match" (($#)) && (($1 == 1)) && write_pipe 0 "$match" else [[ "$line" == "EOF" ]] || unexpected_error "read_pipe" break fi done return 0 } #==============================================================================# write_pipe () # DESCRIPTION: Write information to the named pipe, nonblocking unless it # is told to look for input on stdin and nothing is sent there. Works in # conjunction with read_pipe to make read_pipe non-blocking # INPUT: Tell write_pipe whether information is coming on stdin or from # a parameter, then pass information # OUTPUT: None # PARAMETERS: $1: '0' if passing another parameter(s), '1' if writing to # stdin instead. # $2 and on: If $1 is 0, write_pipe will write the remaining parameters #==============================================================================# { # + is put at the beginning of every line echoed to the pipe, so that # read_pipe can operate in a non-blocking manner local line { [[ "$1" == "0" ]] || [[ "$1" == "1" ]]; } || unexpected_error write_pipe if (($1)); then while IFS= read -r line; do echo "+$line" >&3 done else shift echo "$@" | while IFS= read -r line; do echo "+$line" >&3 done fi return 0 } #-------------------# #@@@@@@@@@@@@@@@@@@@# #---USE LV_API_DO---# #@@@@@@@@@@@@@@@@@@@# #-------------------# # lv_api_do is accessed in the background because it allows for only one # subshell to be invoked when using lv_api_do repeatedly. Makes qq2clone # more efficient (significantly, in some cases) but makes opening and # closing lv_api_do into something that must be managed manually by the # coder #==============================================================================# lv_api_do_check () # DESCRIPTION: See if lv_api_do is present in the expected location. If # not, put it there # INPUT: None # OUTPUT: None # PARAMETERS: None #==============================================================================# { local loc="/run/user/${UID}" [[ -e "${loc}/lv_api_do" ]] && return cd "$loc" || unexpected_error lv_api_do_check echo "$archive" | base64 -d | tar -zx lv_api_do } #==============================================================================# lv_api_do_close () # DESCRIPTION: Tell lv_api_do to exit and close the extra pipe # INPUT: None # OUTPUT: None # PARAMETERS: None #==============================================================================# { echo "exit" >&4 exec 4>&- 4<&- rm -rf "${lv_api_temp:?}" return 0 } #==============================================================================# lv_api_do_comm () # DESCRIPTION: Issue a command to lv_api_do # INPUT: The command # OUTPUT: Return 0/1 on success/failure. lv_api_do output can be accessed # with read_pipe. Exit and error message if lv_api_do encounters # a fatal error # PARAMETERS: $@: command string to lv_api_do #==============================================================================# { # Ensure lv_api_do is open ( : >&4 ; ) &>/dev/null || unexpected_error lv_api_do_comm echo "$*" >&4 local check read -r check <&3 [[ "$check" == "$BAD_REQ" ]] && unexpected_error lv_api_do_comm [[ "$check" == "$NOMATCH" ]] && return 1 [[ "$check" == "$FATAL" ]] && { echo "Error using libvirt API" >&2; exit "$E_libvirt"; } [[ "$check" == "$GOOD" ]] || unexpected_error lv_api_do_comm # This loop avoids a race condition when trying to read_pipe later by # ensuring that lv_api_do has finished its output before this function # returns local line while read -r line <&3; do [[ "$line" == "EOF" ]] && break echo "$line" >&3 done return 0 } #==============================================================================# lv_api_do_open () # DESCRIPTION: Open lv_api_do in background # INPUT: None # OUTPUT: Return 0 on success, exit on failure # PARAMETERS: None #==============================================================================# { declare -g lv_api_temp; lv_api_temp="$(mktemp -d )" || temp_error mkfifo "${lv_api_temp}/lv_api_do_fifo" || fifo_error exec 4<>"${lv_api_temp}/lv_api_do_fifo" "/run/user/${UID}/lv_api_do" <&4 >&3 2>/dev/null & local check read -r check <&3 [[ "$check" == "$CONN_BAD" ]] && lv_api_do_bad_conn [[ "$check" == "$CONN_GOOD" ]] || unexpected_error lv_api_do_open return 0 } #-------------------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---GET/ALTER CONFIGURATION, CHECK SYSTEM---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #-------------------------------------------# #==============================================================================# check_config () #= DESCRIPTION: Given a name or name/value pair, check if it is a #= valid configuration option #= INPUT: Either a name or name value pair #= OUTPUT: Return 1 if the name is not a valid config option or if the #= given value does is not valid for the given name. Return 0 else #= PARAMETERS: $1: Name of config option, $2: (optional) value of option #==============================================================================# { (($#)) || unexpected_error check_config declare -A def_opt def_opt[TEMPLATE]=".*" def_opt[TEMPLATE_DIR]="^/.*" def_opt[QUIET]="^[01]$" def_opt[USE_SPICE]="^[01]$" def_opt[SPICY]="^[01]$" def_opt[S_TIMEOUT]="^[0-9]+$" def_opt[NORUN]="^[01]$" def_opt[STORAGE]="^/.*" (( $# == 1 )) && { [[ " ${!def_opt[*]} " =~ [[:space:]]${1}[[:space:]] ]]; return $?; } local patt="${def_opt["${1}"]}" [[ -n "$patt" ]] || return 1 [[ "$2" =~ $patt ]] || return 1 return 0 } #==============================================================================# check_depends () # DESCRIPTION: Check that required software is present # INPUT: None # OUTPUT: Return 0 on success or exits with descriptive message on failure # PARAMETERS: None #==============================================================================# { local elem missing=0 # These we need to check for local -a depends=( virsh virt-clone virt-xml virt-xml-validate qemu-img \ xmllint sqlite3) # These are virtually certain to be present. But if one is actually missing # or something weird is going on that prevents the script from seeing it, # we'll catch it here and avoid executing code that won't work depends=( "${depends[@]}" base64 basename chmod date dirname file grep less ls md5sum mkfifo mkdir mktemp mv rm sed sort tar touch uniq uuidgen uuidparse vi ) (( BASH_VERSINFO[0] >= 4 )) || { echo "This script must be run with Bash version 4.0+" exit "$E_depends"; } >&2 for elem in "${depends[@]}"; do if ! (unset "$elem"; command -v "$elem") &>/dev/null; then ((missing++)) echo "Missing required software: $elem" >&2 fi done ((missing)) && { echo "This script won't run until you install the listed software" >&2; exit "$E_depends"; } return 0 } #==============================================================================# disp_conf_names () # DESCRIPTION: Display the name and value of all configuration options # INPUT: None # OUTPUT: Echoes config name="value" pairs # PARAMETERS: None #==============================================================================# { local name value while read -r name; do read -r value echo "'$name'='$value'" done < <(sqlite3 "select name,value from CONFIG") return 0 } #==============================================================================# disp_conf_desc () # DESCRIPTION: Display the description of a config option to the user # INPUT: The name of the option # OUTPUT: Echoes relevant lines of information # PARAMETERS: $1: The config option name #==============================================================================# { if [[ "$1" == "TEMPLATE_DIR" ]]; then echo "This is the where template XML files will be kept" echo echo "Default value: '${QQ2_DIR}/templates'" elif [[ "$1" == "TEMPLATE" ]]; then echo "This template will be used for commands like clone, rm, destroy" echo "when option --template/-t is not specified" echo echo "Default value: '' (empty, disabled)" elif [[ "$1" == "QUIET" ]]; then echo "If set to 1, most non-error output will be suppressed" echo echo "Default value: '0'" elif [[ "$1" == "SPICY" ]]; then echo "If set to 1, use spicy as the default spice client instead of" echo "virt-viewer. If virt-viewer is installed during the initial setup," echo "the default value is '1' (enabled). Otherwise, the default value" echo "is '0'" elif [[ "$1" == "USE_SPICE" ]]; then echo "If set to 1, attempt to connect to the spice graphics of a virtual" echo "machine by default when cloning it, if it is configured to use" echo "spice graphics. qq2clone can do this using the programs spicy and" echo "virt-viewer. If either is installed on your system during the" echo "first run, the default value is '1' (enabled). Otherwise, the" echo "default value is '0'" elif [[ "$1" == "S_TIMEOUT" ]]; then echo "Wait this many seconds before timing out when trying to connect to" echo "a virtual machine's spice graphics." echo echo "Default value: '10'" elif [[ "$1" == "NORUN" ]]; then echo "If set to 1, do not automatically run a machine after cloning it." echo echo "Default value: '0'" elif [[ "$1" == "STORAGE" ]]; then echo "The default location to store clone images when creating them." echo "Changing this location is fine, but it is a good idea to ensure" echo "that whatever location you do choose is only used by qq2clone" echo echo "Default value: '${QQ2_DIR}/qq2clone-pool'" else echo "No such configuration option '$1'" return 1 fi return 0 } #==============================================================================# first_run_setup () # DESCRIPTION: Generate a new database with default config values, # create subdirectories of QQ2_DIR # INPUT: None # OUTPUT: None # PARAMETERS: None #==============================================================================# { make_dir "${HOME}/.config" echo "$QQ2_DIR" > "${HOME}/.config/qq2clone" || { echo "Failed to write to config file: ${HOME}/.config/qq2clone" unexpected_error first_run_setup } >&2 # Default locations of key directories local TEMPLATE_DIR="${QQ2_DIR}/templates" local POOL_DIR="${QQ2_DIR}/qq2clone-pool" make_dir "$QQ2_DIR" make_dir "$TEMPLATE_DIR" make_dir "$POOL_DIR" check_rw -r "$QQ2_DIR" local use_spice spicy if command -v virt-viewer &>/dev/null; then use_spice=1 spicy=0 elif command -v spicy &>/dev/null; then use_spice=1 spicy=1 else use_spice=0 spicy=0 fi if [[ -e "${QQ2_DIR}/qq2clone.db" ]]; then echo "A qq2clone database alreadys exists at ${QQ2_DIR}/qq2clone.db" echo "Overwrite this database and create one with default values?" if prompt_yes_no; then check_rw "${QQ2_DIR}/qq2clone.db" rm -f "${QQ2_DIR}/qq2clone.db" || unexpected_error first_run_setup else echo "Setup complete" return 0 fi fi sqlite3 <&2 declare -gA OPT declare -a opts local elem opts=(TEMPLATE_DIR TEMPLATE QUIET USE_SPICE SPICY S_TIMEOUT NORUN \ STORAGE) for elem in "${opts[@]}"; do OPT["$elem"]="$(sqlite3 \ "select value from CONFIG where name=\"$elem\"")" done OPT[COPY_DISKS]=0 # Hardcoded default, overriden with --copy-disks/-C } #==============================================================================# write_config () # DESCRIPTION: Write an option name and value pair to config table. # Checks that the option name and value are good. # INPUT: Name and value of configuration option # OUTPUT: Return 0 on success, 1 on bad option, 2 on bad value # PARAMETERS: $1: Name of variable, $2: Value of variable #==============================================================================# { check_config "$1" || return 1 check_config "$1" "$2" || return 2 sqlite3 "update CONFIG set value='$2' where name='$1';" return 0 } #-----------------------# #@@@@@@@@@@@@@@@@@@@@@@@# #---USAGE INFORMATION---# #@@@@@@@@@@@@@@@@@@@@@@@# #-----------------------# #==============================================================================# usage () # DESCRIPTION: Output basic usage information # INPUT: None # OUTPUT: Echo information to stdout to be read by the user # PARAMETERS: None #==============================================================================# { (( OPT[QUIET] )) && return 0 echo "qq2clone: quick qcow2 clone" echo echo "Description:" echo " Create and manage QEMU/KVM VMs using template machines and qcow2" echo " images with backing files" echo echo "Usage:" echo " qq2clone [OPTION]... [COMMAND] [ARGUMENT]..." echo echo ' options: --connection/-c (URI) --no-spice/-f --help/-h' echo ' --no-run/-n --quiet/-q --quieter/-Q --run/-r --spicy/-S' echo ' --storage/-s (filepath/pool-name) --template/-t (name)' echo ' --use-spice/-g --verbose/-v --virt-viewer/-V' echo echo " commands: check clone config connect copy-template copyright" echo " delete-template destroy edit exec import-template" echo " license list list-templates modify-template restore" echo " resume rm rm-wipe rm-shred save save-rm setup start" echo " suspend" echo echo " For more information, see: man qq2clone" return 0 } #-----------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---INPUT/OUTPUT HELPER FUNCTIONS---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #-----------------------------------# #==============================================================================# prompt_num () # DESCRIPTION: Prompt user for a number between $1 and $2 # INPUT: Inclusive endpoints of accepted interval, where the right hand # endpoint is less than 10 # OUTPUT: Echo choice when proper input is received # PARAMETERS: $1: LH of interval, $2: RH of interval #==============================================================================# { { (( $1 > -1 )) && (( $1 < $2 )) && (( $2 < 10 )) ; } || \ unexpected_error prompt_num local n read -rsn 1 n { [[ "$n" =~ ^[0-9]$ ]] && (($1 <= n)) && ((n <= $2)); } || { echo "Enter a number from $1 to $2" >&2; prompt_num "$1" "$2"; return 0; } echo "$n" return 0 } #==============================================================================# prompt_yes_abort () # DESCRIPTION: Prompt user to enter y, or any other key to abort # INPUT: A keystroke # OUTPUT: Prompts for input, returns 0 for Y/y and 1 for else # PARAMETERS: $1, $2: (Optional) override disp with $1 and patt with $2 #==============================================================================# { local disp="Press (y) to accept, anthing else to abort" patt="^[Yy]$" [[ -n "$1" ]] && disp="$1" [[ -n "$2" ]] && patt="$2" local char echo "$disp" read -rn 1 char echo [[ "$char" =~ $patt ]] && return 0 return 1 } #==============================================================================# prompt_yes_no () # DESCRIPTION: Prompt user to enter y or n, repeatedly until they do # INPUT: Keystrokes # OUTPUT: Prompts for input, returns 1 for N/n or 0 for Y/y # PARAMETERS: None #========================================================================= # { local char until [[ "$char" =~ ^[YyNn]$ ]]; do echo "Press (y)/(n) for yes/no" read -rn 1 char echo done [[ "$char" =~ ^[Nn]$ ]] && return 1 return 0 } #-----------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---PARSE/SEARCH/MODIFY XML---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #-----------------------------# #==============================================================================# do_virt_xml () # DESCRIPTION: Run a given virt-xml command, reading from and writing to # the named pipe # INPUT: Parameters to send to virt-xml # OUTPUT: Nothing except on error # PARAMETERS: $@: Passed to virt-xml #==============================================================================# { local xml xml="$(virt-xml "$@" <<<"$(read_pipe)" 2>/dev/null)" || { echo "Attempt to generate xml with virt-xml failed." echo "This is probably a bug in qq2clone" exit "$E_unexpected" ; } >&2 write_pipe 1 <<<"$xml" return 0 } #==============================================================================# find_tag () # DESCRIPTION: Use xmllint to do an xpath search of xml and write_pipe # all matches # INPUT: Xpath and XML # OUTPUT: Write one match per line # PARAMETERS: $1: Xpath, $2: XML location, or leave blank to read from # stdin #==============================================================================# { if [[ -n "$2" ]]; then write_pipe 1 <"$2" else local line while read -r line; do write_pipe 0 "$line" done fi xmllint --xpath "$1" --auto |& grep -qi 'xpath error' && unexpected_error find_tag xmllint --noblanks --dropdtd --nowarning --xpath "$1" \ 2>/dev/null <(read_pipe) | write_pipe 1 return 0 } #==============================================================================# get_attr_value () # DESCRIPTION: Given an attribute="value" pair, echo the value # INPUT: Attribute value pair # OUTPUT: Value, or unexpected_error if input doesn't match pattern (do not # rely on this function for checking that a string is a name=value pair # PARAMETERS: $1: attribute="value" #==============================================================================# { p="$(strip_ws "$1")" [[ "$p" =~ ^[^\=]+\=[[:space:]]*[^[:space:]](.*).$ ]] && { echo "${BASH_REMATCH[1]}"; return 0; } unexpected_error get_attr_value return 0 } #==============================================================================# get_disk_devices () # DESCRIPTION: Find all disk device file locations from an XML file # INPUT: libvirt domain XML file on stdin # OUTPUT: writepipe each file location # PARAMETERS: None #==============================================================================# { find_tag '//devices/disk[@type="file"][@device="disk"]/source/@file' local line val while read -r line; do val="$(get_attr_value "$line")" || unexpected_error get_disk_devices write_pipe 0 "$val" done < <(read_pipe) return 0 } #==============================================================================# get_disk_devices_db () # DESCRIPTION: Like get_disk_devices, but get info from the database # INPUT: Machine number, or omit to get template info # OUTPUT: writepipe each file location # PARAMETERS: $1: (Optional) machine number #==============================================================================# { local query disk if (($#)); then query="select disks from CLONES where id='$1' \ and template='${OPT[TEMPLATE]}';" else query="select disks from TEMPLATES where name='${OPT[TEMPLATE]}';" fi while read -r disk; do write_pipe 0 "$disk" done < <(sqlite3 "$query") return 0 } #==============================================================================# has_spice () # DESCRIPTION: Check whether a machine supports spice graphics # INPUT: A machine number # OUTPUT: Returns 0 if yes and 1 if no # PARAMETERS: $1: A machine number #==============================================================================# { local uuid uuid="${CL_MAP["$1"]}" find_tag '//devices/graphics[@type="spice"]' < <(virsh dumpxml "$uuid") local match=0 line while read -r line; do match=1 done < <(read_pipe) ((match)) && return 0 return 1 } #--------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---FILESYSTEM/DB INTERACTIONS---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #--------------------------------# #==============================================================================# copy_file () # DESCRIPTION: Copy file $1 to $2 and give error messages, exit on error # INPUT: File to copy # OUTPUT: Error messages and exit codes as needed # PARAMETERS: $1: File to copy, $2: Location to copy to #==============================================================================# { (($# == 2)) || unexpected_error copy_file check_rw "$1" "$(dirname "$2")" [[ -e "$2" ]] && check_rw "$2" cp -fR "$1" "$2" &>/dev/null || unexpected_error copy_file return 0 } #==============================================================================# get_md5 () # DESCRIPTION: Get md5sum of a file without the trailing filename # INPUT: A filepath # OUTPUT: The md5sum # PARAMETERS: $1: Filepath #==============================================================================# { local md5 check_rw "$1" || unexpected_error get_md5 md5="$(md5sum "$1")" [[ "$md5" =~ ^[[:space:]]*([^[:space:]]+)([[:space:]]|$) ]] echo "${BASH_REMATCH[1]}" return 0 } #==============================================================================# make_dir () # DESCRIPTION: Make a directory at given location or exit with error # message # INPUT: Filepath # OUTPUT: Error messages and exit code as needed # PARAMETERS: $1: Filepath of directory to make #==============================================================================# { (($# == 1)) || unexpected_error make_dir if [[ -e "$1" ]]; then [[ -d "$1" ]] || { echo "Tried to create directory:" echo "$1" echo "but it already exists and is not a directory" exit "$E_file"; } >&2 check_rw "$1" return 0 fi mkdir -p "$1" &>/dev/null check_rw "$1" return 0 } #==============================================================================# move_file () # DESCRIPTION: Move file $1 to $2 or give error messages, exit on error # INPUT: File to move, new location # OUTPUT: Error messages and exit codes as needed # PARAMETERS: $1: File to move, $2: Location to move to #==============================================================================# { (($# == 2)) || unexpected_error move_file check_rw "$1" "$(dirname "$2")" if [[ -e "$2" ]]; then chmod +rw "$2" || { echo "No permission to write $2" >&2; exit "$E_permission"; } fi mv -f "$1" "$2" &>/dev/null || unexpected_error move_file return 0 } #==============================================================================# write_file () # DESCRIPTION: Write contents of named pipe to file or error and exit # INPUT: Filepath as parameter, content via write_pipe # OUTPUT: Error messages and exit codes as needed # PARAMETERS: $1: Filepath #==============================================================================# { (($# == 1)) || unexpected_error write_file touch "$1" check_rw "$1" [[ -d "$1" ]] && unexpected_error write_file local temp1 temp2 temp1="$(mktemp)" || temp_error temp2="$(mktemp)" || { rm -f "$temp1" &>/dev/null; temp_error; } cp "$1" "$temp1" || { rm -f "$temp1" "$temp2" &>/dev/null; unexpected_error write_file; } read_pipe > "$temp2" || { rm -f "$temp1" "$temp2" &>/dev/null; unexpected_error write_file; } mv -f "$temp2" "$1" &> /dev/null || { rm -f "$1" &>/dev/null; mv -f "$temp1" "$1" &>/dev/null; rm -f "$temp1" "$temp2" &>/dev/null; unexpected_error write_file; } return 0 } #==============================================================================# sqlite3 () # DESCRIPTION: Pass arguments to sqlite3 binary, prepending basic # parameters that are always used # INPUT: Arguments to sqlite3 # OUTPUT: Dependent on sqlite3 # PARAMETERS: Arbitrary #==============================================================================# { $(unset sqlite3; command -v sqlite3) --batch --separator $'\n' \ "${QQ2_DIR}/qq2clone.db"\ "$@" || unexpected_error sqlite3 } #-----------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---IMPORT/MODIFY TEMPLATES---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #-----------------------------# #==============================================================================# commit_image () # DESCRIPTION: Commit changes from staging image(s) to template image(s) # INPUT: Parameters to calling function # OUTPUT: Status updates, prompts, error messages # PARAMETERS: $@ from calling function #==============================================================================# { if (( ${#CL_MAP[@]} + ${#BAD_CL[@]} > 1)); then echo "Cannot commit image while there are clones. Aborting." >&2 exit "$E_args" fi local disk check t d get_disk_devices_db while read -r disk; do while read -r t; do check="$(sqlite3 "select exists \ (select * from CLONES where template='$t');")" ((check)) || continue while read -r d; do if [[ "$d" == "$disk" ]]; then echo "Although this template has no clones, template" echo "$t does. These templates share disk:" echo echo " $disk" echo echo "If changes are committed, these clones may become" echo "corrupted. To avoid this, retrieve any information you" echo "need from these clones and then delete them. Aborting." exit "$E_args" fi >&2 done < <(sqlite3 "select disks from TEMPLATES where name='$t';") done < <(sqlite3 "select name from TEMPLATES where \ not name='${OPT[TEMPLATE]}';") done < <(read_pipe) if (($# < 3)); then echo "This operation has the potential to corrupt your master template" echo "image if it is interrupted." if ((OPT[QUIET] )); then echo "Append argument force to continue" >&2 exit "$E_args" fi prompt_yes_abort || exit 0 fi while read -r disk; do ((OPT[QUIET])) || echo "Committing $disk..." output="$(qemu-img commit -d "$disk" 2>&1)" || { echo "$output"; echo "Operation failed"; exit "$E_unexpected"; } >&2 rm -f "$disk" &>/dev/null || { echo "Failed to delete old image. Permission issue?"; echo "Process may not have completed succesfully"; exit "$E_permission"; } >&2 done < <(sqlite3 "select disks from CLONES where id='0' and\ template='${OPT[TEMPLATE]}';") delete_machine 0 0 return 0 } #==============================================================================# get_template_name () # DESCRIPTION: Helper for exec_com_import_template. write_pipes the # original name from the xml, then the new one # INPUT: XML is write_piped, and argument to exec_com_import_template # giving the name to import template with is optionally provided # OUTPUT: See description. Error messages if name is bad # PARAMETERS: $1: (optional) Template name, overrides value from XML #==============================================================================# { local name char name="$(strip_ws "$1")" if [[ -n "$name" ]]; then valid_xml_name_check "$name" template_name_available "$name" fi local xmlname find_tag '//name/text()' <<<"$(read_pipe)" while read -r line; do line="$(strip_ws "$line")" if [[ -n "$xmlname" ]]; then xmlname="${xmlname}${line}" else xmlname="$line" fi done < <(read_pipe) write_pipe 0 "$xmlname" if [[ -z "$name" ]]; then name="$xmlname" fi write_pipe 0 "$name" return 0 } #==============================================================================# import_get_xml () # DESCRIPTION: Determine if argument to exec_com_import_template is a # libvirt domain on the current connection or a filepath, check that it # is valid, and write_pipe the xml # INPUT: argument designating template XML or domain name/uuid # OUTPUT: Error messages as needed, write_pipes XML on success # PARAMETERS: $1: $1 from calling funcion #==============================================================================# { if [[ "$1" =~ ^/ ]]; then { [[ -e "$1" ]] && [[ -r "$1" ]] ; } || { echo "No read permission for $1 or file does not exist" exit "$E_permission" ; } >&2 virt-xml-validate "$1" &> /dev/null || { virt-xml-validate "$1"; echo "File $1 is not a valid libvirt domain XML document" exit "$E_xml" ; } >&2 write_pipe 1 <"$1" else virsh dominfo "$1" &>/dev/null || { echo "Cannot access libvirt domain with name/uuid $1. " echo "Wrong connection URI? Currently $LIBVIRT_DEFAULT_URI" exit "$E_libvirt"; } >&2 local line uuid while read -r line; do if [[ "$line" =~ \ ^[[:space:]]*[Uu][Uu][Ii][Dd]\:[[:space:]]*([^[:space:]]+) ]]; then uuid="${BASH_REMATCH[1]}" fi done < <(virsh dominfo "$1") local check=0 check="$(sqlite3 "select exists (select * from CLONES \ where uuid='$uuid');")" if ((check)); then echo "Cannot import a clone as a template" >&2 exit "$E_template" fi virsh dumpxml --inactive "$1" 2>/dev/null | write_pipe 1 fi return 0 } #==============================================================================# rename_template () # DESCRIPTION: Change template name, and all of its clone names # INPUT: A current template name, and a new one # OUTPUT: Status updates, error messages # PARAMETERS: $@ from calling function exec_com_modify_template #==============================================================================# { local old_name="$1" new_name="$3" local tdir="${OPT[TEMPLATE_DIR]}" local xml="${tdir}/${1}.xml" check_rw "$xml" OPT[TEMPLATE]="$old_name" write_pipe 1 <"$xml" template_name_available "$new_name" valid_xml_name_check "$new_name" do_virt_xml --edit --metadata name="$new_name" xml="${tdir}/${new_name}.xml" write_file "$xml" rm -f "${tdir}/${old_name}.xml" &>/dev/null sqlite3 "update TEMPLATES set name='${new_name}' where name='${old_name}';" OPT[TEMPLATE]="$new_name" check_template &>/dev/null # Just to update md5sum ((OPT[QUIET])) || echo "Template name changed"; if (( ${#CL_MAP[@]} + ${#BAD_CL[@]} )); then if ! ((OPT[QUIET] == 2)); then echo "Now renaming clones" local machines_on machines_on="$(get_target_set destroy)" [[ -n "$machines_on" ]] && { echo "All clones that are not turned off will not be renamed." echo "qq2clone will still know they are clones of $new_name," echo "but in virsh and virt-viewer their old name will remain." echo echo "Shut down any running clones of $new_name you wish renamed" echo "and press enter when ready to proceed." read -rs echo ; } >&2 fi local id uuid cl_name while read -r id; do read -r uuid cl_name="$(unique_name_uuid 0 "${new_name}#$id")" virsh domrename "$uuid" "$cl_name" &>/dev/null sqlite3 "update CLONES set template='$new_name' where\ template='$old_name';" done < <( sqlite3 "select id,uuid from CLONES where \ template='$old_name'" ) fi (( OPT[QUIET] )) || echo "Template rename complete" exit 0 } #==============================================================================# user_undefine_domain () # DESCRIPTION: Prompt the user to undefine libvirt domain (or not) # INPUT: Domain name # OUTPUT: Gives info to and prompts user # PARAMETERS: $1: Domain name #==============================================================================# { ((OPT[QUIET] == 2)) && return 0 echo echo "Would you like to undefine the libvirt domain this template" echo "was made from? This prevents the original domain from being" echo "accidentally run, altering the template disk and potentially" echo "corrupting any clones of that template." echo echo "This will apply every flag listed in 'man virsh' for command" echo "'undefine' that may be required to succeed. I.e., snapshot" echo "and checkpoint metadata, managed save images, etc. will be" echo "discarded." echo if prompt_yes_no; then virsh domstate "$1" 2>/dev/null | grep -q "shut off" || virsh domstate "$1" 2>/dev/null | grep -q "crashed" || { echo "This domain is still running, so make sure you turn it off"; echo "before making clones from this template. Otherwise, it will" echo "continue modifying the template storage device even" echo "though it is undefined."; } >&2 if virsh undefine --managed-save --checkpoints-metadata\ --snapshots-metadata --nvram "$1" &> /dev/null; then echo "Domain undefined." else echo "Could not undefine domain. Import still completed." fi fi } #-------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---ERROR MESSAGES AND CHECKS---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #-------------------------------# #==============================================================================# arg_error () # DESCRIPTION: If args are too few, too many, or simply wrong, this # function provides a concise way to exit with proper message and code # INPUT: Type of problem, name of command, name of bad arg # OUTPUT: Echo error message, exit with E_args # PARAMETERS: $1: 0) too few args 1) too many args 2) incorrect arg # $2: The command, $3: in the case of incorrect arguments, the bad # argument in question (omit otherwise) #==============================================================================# { local line if (( $1 == 0 )); then echo "Too few arguments to qq2clone $2" elif (( $1 == 1 )); then echo "Too many arguments to qq2clone $2" else echo "Bad argument to qq2clone $2: $3" fi exit "$E_args" } >&2 #==============================================================================# check_dir () # DESCRIPTION: Checks that a directory can be written to # INPUT: A filepath # OUTPUT: Error messages and exit codes as needed # PARAMETERS: $1: Filepath #==============================================================================# { [[ "$1" =~ ^/ ]] || { echo "Invalid filepath $1 specified. Use an absolute filepath"; exit "$E_args"; } mkdir -p "$1" &>/dev/null { [[ -d "$1" ]] && [[ -w "$1" ]]; } || { echo "Cannot create $1 or cannot write to directory, check "; echo "filepath and permissions"; exit "$E_permission"; } return 0 } >&2 #==============================================================================# check_rw () # DESCRIPTION: Provide an error message and exit if specified file cannot # be read and written to. If file is a directory and a preceding # argument is '-r', check recursively # INPUT: A filepath (preferably fully qualified as a file could technically # be named '-r') # OUTPUT: Error messages and exit codes as needed # PARAMETERS: $@: Filepaths to check #==============================================================================# { local redir if [[ "$1" == "-r" ]]; then redir=1; shift else redir=0 fi while (($#)); do if { chmod +rw "$1" || { [[ -w "$1" ]] && [[ -r "$1" ]]; } || readlink "$1" ; } &>/dev/null; then shift elif [[ -e "$1" ]]; then echo "No read/write permissions for $1" >&2 exit "$E_permission" else echo "The filepath $1 either does not exist or cannot be seen " >&2 echo "with current permissions" exit "$E_permission" fi local type line type="$(file -b "$1")" if [[ "$type" =~ directory ]] && ((redir)); then while read -r line; do check_rw -r "$line" done < <(find "$1" 2>/dev/null) fi done return 0 } >&2 #==============================================================================# check_template () # DESCRIPTION: Check if OPT[TEMPLATE] is defined. If it is, see if its # md5sum is in agreement with the database. If it isn't, update the # database and see if it is valid. Make sure that aspect of the db is # updated too. Return 1 if template is not defined, or 2 if it is not # valid # INPUT: None # OUTPUT: Error message and exit # PARAMETERS: None #==============================================================================# { local md5 md5_curr valid check_template_exists || return 1 md5="$(sqlite3 "select md5sum from TEMPLATES where \ name='${OPT[TEMPLATE]}';")" [[ -e "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" ]] || { sqlite3 "update TEMPLATES set md5sum='0',valid='0' \ where name='${OPT[TEMPLATE]}';"; return 2; } valid="$(sqlite3 "select valid from TEMPLATES where \ name='${OPT[TEMPLATE]}';")" check_rw "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" local md5_curr md5_curr="$(get_md5 "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml")" [[ "$md5" == "$md5_curr" ]] && [[ "$valid" == "1" ]] && return 0 [[ "$md5" == "$md5_curr" ]] && [[ "$valid" == "0" ]] && return 2 [[ "$md5" == "$md5_curr" ]] && unexpected_error check_template valid=0 virt-xml-validate "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" \ &>/dev/null && valid=1 sqlite3 "update TEMPLATES set md5sum='$md5_curr',valid='$valid' \ where name='${OPT[TEMPLATE]}';" local disks if ((valid)); then get_disk_devices < "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" disks="$(read_pipe)" sqlite3 "update TEMPLATES set disks='$disks',valid='$valid' \ where name='${OPT[TEMPLATE]}';" fi ((valid)) || return 2 return 0 } #==============================================================================# check_template_disks () # DESCRIPTION: Verify that the disks named by a template exist, can be read # and are not locked. This check is not needed for most commands, but # when it is needed, check_template should be succesfully run first to # verify that the disks column in the database is correct # INPUT: None # OUTPUT: Error messages and exit if needed # PARAMETERS: None #==============================================================================# { local disk qemu_out while read -r disk; do [[ -e "$disk" ]] || { echo "Template ${OPT[TEMPLATE]} refers to $disk, which either"; echo "does not exist or cannot be seen with current permissions"; exit "$E_template"; } >&2 [[ -r "$disk" ]] || { echo "Template ${OPT[TEMPLATE]} refers to $disk, but the file" ; echo "cannot be read"; exit "$E_permission"; } qemu_out="$(qemu-img info "$disk" 2>&1)" || { echo "When checking the disk file $disk with qemu-img, the"; echo "following problem was encountered:"; echo "$qemu_out"; exit "$E_libvirt"; } >&2 done < <( sqlite3 "select disks from TEMPLATES where \ name='${OPT[TEMPLATE]}';") return 0 } #==============================================================================# check_template_exists () # DESCRIPTION: There are a few places where it is necessary to check that # a template exists, but not the rest of check_template, so this is its # own function # INPUT: None # OUTPUT: Return 0 if OPT[TEMPLATE] exists and 1 if it does not # PARAMETERS: None #==============================================================================# { local check check="$(sqlite3 "select exists ( select * from TEMPLATES where\ name='${OPT[TEMPLATE]}');")" ((check)) && return 0 return 1 } #==============================================================================# fifo_error () # DESCRIPTION: Error to display if fifo creation files # INPUT: None # OUTPUT: Error message and exit code # PARAMETERS: None #==============================================================================# { echo "Cannot make fifo" exit "$E_extcom" } >&2 #==============================================================================# lv_api_do_bad_conn () # DESCRIPTION: Error displayed when lv_api_do cannot connect to API # INPUT: None # OUTPUT: Error message and exit code # PARAMETERS: None #==============================================================================# { echo "Cannot connect to libvirt API" exit "$E_libvirt" } 2>/dev/null #==============================================================================# set_error () # DESCRIPTION: Used when convert_to_seq fails # INPUT: None # OUTPUT: Error message and exit # PARAMETERS: None #==============================================================================# { echo "Improper or badly formatted argument specifying machine or set of " echo "machines" exit "$E_args" } >&2 #==============================================================================# target_error () # DESCRIPTION: Used when intersection of user-specified set and set of # existing machines that are valid targets for current command results in # empty set # INPUT: Name of command # OUTPUT: Error message and exit # PARAMETERS: $1: Name of command invoked with set #==============================================================================# { echo "Specified set of machines does not contain any valid targets" echo "for $1 to operate on" exit "$E_args" } >&2 #==============================================================================# stage_error () # DESCRIPTION: When an action is attempted on a 'staging' clone, (one # created by modify-template prepare-image) but that clone is listed in # BAD_CL, this message is displayed # INPUT: None # OUTPUT: Error message and exit # PARAMETERS: None #==============================================================================# { echo "A clone staging changes to the template iamge was previously made," echo "but it is now missing. Restore it manually, connect to the" echo "appropriate URI if it is on another connection, or delete it from" echo "qq2clone's database using the command:" echo echo " qq2clone check ${OPT[TEMPLATE]}" echo echo "(This clone will have ID: 0)" exit "$E_permission" } >&2 #==============================================================================# temp_error () # DESCRIPTION: If mktemp fails, this function should be invoked # INPUT: None # OUTPUT: Error message and exit with E_extcom # PARAMETERS: None #==============================================================================# { echo "Attempt to create a temp file with mktemp failed" exit "$E_extcom" } >&2 #==============================================================================# template_error () # DESCRIPTION: Takes a return code from check_template, gives appropriate # error message and exits if it is nonzero # INPUT: Check_template return status # OUTPUT: Error message and exit code or nothing # PARAMETERS: $1: Return code from check_template #==============================================================================# { (($1 == 1)) && { echo "The template '${OPT[TEMPLATE]}' does not exist"; exit "$E_template"; } (($1 == 2)) && { echo "The template '${OPT[TEMPLATE]}' is not valid due to bad"; echo -n "or missing XML at location:"; echo echo " ${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml"; echo exit "$E_template"; } return 0 } >&2 #==============================================================================# template_name_available () # DESCRIPTION: Check that the template name is available, and give an # appropriate error message if not # INPUT: A name # OUTPUT: An error message if needed # PARAMETERS: $1: Template name to check #==============================================================================# { local name while IFS= read -r name; do if [[ "$name" == "$1" ]]; then echo "The name $1 belongs to an existing template" exit "$E_template" fi done < <(sqlite3 "select name from TEMPLATES;") if [[ -e "${OPT[TEMPLATE_DIR]}/${1}.xml" ]]; then echo "Although template name $1 is not currently in use," echo "a file where this template's XML document belongs already" echo "exists. Move or delete this file:" echo echo " ${OPT[TEMPLATE_DIR]}/${1}.xml" exit "$E_template" fi return 0 } >&2 #==============================================================================# unexpected_error () # DESCRIPTION: Error on unexpected event, which is likely a bug in qq2clone # INPUT: None # OUTPUT: Error message and exit code # PARAMETERS: $1: function name where error occurred #==============================================================================# { echo "qq2clone has encountered an unexpected problem." echo "The problem occurred in function: $1" exit "$E_unexpected" } >&2 #==============================================================================# valid_xml_name_check () # DESCRIPTION: Check that XML is valid after modifying name and return 0 # if so, exit with error message else # INPUT: write_piped XML file and new name as parameter. Leaves XML in # pipe after execution # OUTPUT: Error message and exit code if needed # PARAMETERS: $1: The new name #==============================================================================# { virt-xml --edit --metadata name="$1"<<<"$(read_pipe 1)" &>/dev/null || { echo "When trying to use name $1 to generate an xml" echo "file, there was an error - this name is not acceptable" echo "for a libvirt domain. Try another." exit "$E_libvirt" ; } >&2 } >&2 #--------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---HELPERS FOR EXEC_COM_CHECK---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #--------------------------------# #==============================================================================# delete_template_and_clones () # DESCRIPTION: Delete a template and all of its clones # INPUT: None # OUTPUT: Status updates # PARAMETERS: None #==============================================================================# { echo hr echo echo "DELETING TEMPLATE: ${OPT[TEMPLATE]}" local id disk echo local fail=0 echo " Deleting all defined clone domains" for id in "${!CL_MAP[@]}"; do echo " Attempting to delete ${OPT[TEMPLATE]}#${id} ${CL_MAP["$id"]}..." if ( delete_machine "$id" ) &> /dev/null; then echo " Success." else echo " Failed." fail=1 fi done if ((fail)) || (( ${#BAD_CL[@]} )); then echo echo " Manually deleting files and/or undefining any remaining domains" while read -r id; do while read -r disk; do [[ -z "$disk" ]] && continue rm -f "$disk" &>/dev/null || [[ -e "$disk" ]] && { echo "Failed to delete $disk, check permissions. Aborting" >&2; exit "$E_permission"; } done < <(sqlite3 "select disks from CLONES where id='$id' and \ template='${OPT[TEMPLATE]}'") sqlite3 "delete from CLONES where id='$id' and \ template='${OPT[TEMPLATE]}';" echo " Deleted ${OPT[TEMPLATE]}#${id} ${CL_MAP["$id"]}" done < <(sqlite3 "select id from CLONES where \ template='${OPT[TEMPLATE]}';") fi echo echo " All clones deleted." echo rm -f "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" &>/dev/null if [[ -e "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" ]]; then echo "Failed to delete template XML at" >&2 echo "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" >&2 echo "Aborting" exit "$E_permission" fi sqlite3 "delete from TEMPLATES where name='${OPT[TEMPLATE]}';" echo "TEMPLATE DELETED: Template ${OPT[TEMPLATE]} deleted." return 0 } #==============================================================================# prompt_delete_bad_clones () # DESCRIPTION: Iterate through missing clones, prompting user before # taking action # INPUT: None # OUTPUT: Prompts and status updates to user # PARAMETERS: None #==============================================================================# { local id i=0 total="${#BAD_CL[@]}" disk prompt=1 select local t="${OPT[TEMPLATE]}" for id in "${!BAD_CL[@]}"; do ((i++)) echo hr echo echo "MISSING CLONE ${i} / $total" echo " ID: $id" echo "UUID: ${BAD_CL["$id"]}" while read -r disk; do echo "DISK: $disk" done < <(sqlite3 "select disks from CLONES where id='$id' and\ template='${t}';") echo if ((prompt)); then echo " (1) Delete clone from database, DO NOT delete disk files" echo " (2) Delete clone from database, DO delete disk files" echo " (3) Skip this clone" echo " (4) Do option (1) for all missing clones of this template" echo " (5) Do option (2) for all missing clones of this template" echo " (6) Abort: leave the clones as they are" select="$(prompt_num 1 6)" (( select == 6 )) && { echo "Abort"; return 0; } (( select == 5 )) && { select=2; prompt=0; } (( select == 4 )) && { select=1; prompt=0; } (( select == 3 )) && { echo "Skipping"; echo; continue; } fi echo if ((select==2)); then while read -r disk; do [[ -e "$disk" ]] || { echo " $disk :" echo " Already deleted or has been moved" continue; } if rm -f "$disk" &>/dev/null; then echo " Deleted $disk" else echo " Failed to delete $disk" fi done < <(sqlite3 "select disks from CLONES where id='$id' and\ template='${t}';") fi sqlite3 "delete from CLONES where id='$id' and template='$t';" echo " Clone deleted from database" echo done return 0 } #-----------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---INTERACT WITH VIRSH/VMs---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #-----------------------------# #==============================================================================# clone () # DESCRIPTION: Clone a virtual machine from OPT[TEMPLATE] # INPUT: If desired, designate that clone should have special ID 0 # OUTPUT: Echo message when complete or on error # PARAMETERS: $1: (Optional) If '0', create clone intended for staging # changes to a base template image #==============================================================================# { local base_mach_name line check i local txml="${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" if [[ "$1" == "0" ]]; then # ID reserved for clone where changes to template image are staged i="0" check="$(sqlite3 "select exists ( select * from CLONES where id='0' and \ template='${OPT[TEMPLATE]}');")" ((check)) && return 1 base_mach_name="${OPT[TEMPLATE]}#STAGING" else for((i=1;;i++)); do check="$(sqlite3 "select exists ( select * from CLONES where id='$i' \ and template='${OPT[TEMPLATE]}');")" (("$check")) || break done base_mach_name="${OPT[TEMPLATE]}#${i}" fi local name uuid { read -r name read -r uuid } < <(unique_name_uuid 2 "$base_mach_name") local storage="${OPT[STORAGE]}" declare -a f_arr local img new_img j=-1 type local disks disks="$(sqlite3 "select disks from TEMPLATES where\ name='${OPT[TEMPLATE]}';")" trap 'rm -f "${storage}/${name}.${uuid:?}.*"' INT while read -r img; do ((j++)) new_img="${storage}/${name}.${uuid}.${j}.qcow2" type="$(get_format "$img")" qemu-img create -f qcow2 -F "$type" -b "$img" "$new_img"\ &>/dev/null || { rm -f "${storage}/${name}.${uuid:?}.*"; unexpected_error clone; } f_arr[${#f_arr[@]}]="-f" f_arr[${#f_arr[@]}]="$new_img" done < <(echo "$disks") virt-clone --original-xml "$txml" --name "$name" --uuid "$uuid"\ --preserve-data "${f_arr[@]}" &>/dev/null || { rm -f "${storage}/${name}.${uuid:?}.*"; unexpected_error clone; } disks="" local before=0 for ((j=1;j<${#f_arr[@]};j+=2)); do if ((before)); then disks="$(echo "$disks";echo "${f_arr["$j"]}")" else before=1 disks="${f_arr["$j"]}" fi done sqlite3 "insert into CLONES values \ ('$uuid','$i','${OPT[TEMPLATE]}','$disks');" ((OPT[QUIET])) || echo "Cloned: $name $uuid" CL_MAP["$i"]="$uuid" CL_STATE["$i"]="off" trap 'exit' INT if (( OPT[NORUN] )); then return 0 elif ((OPT[USE_SPICE])); then connect "$i" else start_domain "$i" fi return 0 } #==============================================================================# connect () # DESCRIPTION: Run machine. If it has spice graphics, connect to graphical # console with virt-viewer/spicy # INPUT: Machine number # OUTPUT: None except on error # PARAMETERS: $1: Machine number #==============================================================================# { if (( OPT[SPICY] )); then command -v spicy &> /dev/null || { echo "Cannot find command spicy" >&2; exit "$E_extcom"; } else command -v virt-viewer &> /dev/null || { echo "Cannot find command virt-viewer" >&2; exit "$E_extcom"; } fi start_domain "$1" local uuid uuid="${CL_MAP["$1"]}" has_spice "$1" || return 0 local spice read -ra spice < <(get_spice "$1") || { echo "Machine did not become available before timeout" >&2; exit "$E_timeout"; } (( $1 == 0 )) && set -- "STAGING" if (( OPT[SPICY] )); then command -v spicy &> /dev/null || { echo "Cannot find command spicy" >&2; exit "$E_extcom"; } nohup spicy --title "${OPT[TEMPLATE]}#$1" -h "${spice[0]}" \ -p "${spice[1]}" &>/dev/null & else command -v virt-viewer &> /dev/null || { echo "Cannot find command virt-viewer" >&2; exit "$E_extcom"; } nohup virt-viewer --uuid "$uuid" &>/dev/null & fi return 0 } #==============================================================================# copy_disks () # DESCRIPTION: Go through XML file, find all disk images, and copy them. # A base name is provided, and new files placed in OPT[STORAGE] # INPUT: XML file in pipe # OUTPUT: Altered XML in pipe, and new image files created # PARAMETERS: $1: Base name for disks #==============================================================================# { (($#==1)) || unexpected_error copy_disks local elem i=0 name xml declare -a disks xml="$(read_pipe 1)" [[ -n "$xml" ]] || unexpected_error copy_disks read_pipe | get_disk_devices while read -r line; do disks=("${disks[@]}" "$line"); done < <(read_pipe) write_pipe 1 <<<"$xml" RANDOM="$$" for elem in "${disks[@]}"; do ((i++)) name="${1}.${i}" while [[ -e "${OPT[STORAGE]}/${name}.qcow2" ]]; do name="${1}.${i}-${RANDOM}" done ((OPT[QUIET])) || echo "Copying disk ${elem}..." copy_file "$elem" "${OPT[STORAGE]}/${name}.qcow2" do_virt_xml --edit path="$elem" --disk \ path="${OPT[STORAGE]}/${name}.qcow2" done return 0 } #==============================================================================# delete_machine () # DESCRIPTION: Delete a clone # INPUT: Machine number # OUTPUT: None # PARAMETERS: $1: machine number, $2: 0 to just delete disks, 1 to wipe, 2 # to shred #==============================================================================# { local uuid uuid="${CL_MAP["$1"]}" local line disks before=0 while read -r line; do ((before)) && disks="${disks},"; before=1 disks="${disks}$line" done < <(sqlite3 "select disks from CLONES where id='$1' and\ template='${OPT[TEMPLATE]}';") declare -a undef_args undef_args=( '--managed-save' '--snapshots-metadata' '--nvram' \ '--checkpoints-metadata' ) if (( $2 < 2 )); then undef_args=( "${undef_args[@]}" '--storage' "$disks") fi if (($2==1)); then undef_args=( "${undef_args[@]}" "--wipe-storage" ) fi destroy_domain "$1" &>/dev/null if (( $2 == 2 )); then local disk echo "Shredding ${OPT[TEMPLATE]}#$1 disks..." while read -r disk; do { cd "$(dirname "$disk")" && { shred -vf "$(basename "$disk")" 2>&1 | sed "s/^./ &/"; } && rm -f "$disk" &>/dev/null } || unexpected_error delete_machine done < <(sqlite3 "select disks from CLONES where id='$1' and\ template='${OPT[TEMPLATE]}';") fi local virsh_out virsh_out="$(virsh undefine "$uuid" "${undef_args[@]}" 2>&1)" || { echo "$virsh_out"; unexpected_error delete_machine; } sqlite3 "delete from CLONES where id='$1' and \ template='${OPT[TEMPLATE]}';" return 0 } #==============================================================================# destroy_domain () # DESCRIPTION: Invoke virsh destroy on given machine # INPUT: A machine number # OUTPUT: None # PARAMETERS: A machine number #==============================================================================# { local uuid uuid="${CL_MAP["$1"]}" virsh destroy "$uuid" &>/dev/null return 0 } #==============================================================================# discard_save () # DESCRIPTION: Delete a saved state file associated with a clone # INPUT: A saved machine number # OUTPUT: Error message, exit code if needed # PARAMETERS: $1: A saved machine number #==============================================================================# { local virsh_out virsh_out="$(virsh managedsave-remove "${CL_MAP["$1"]}" 2>&1)" || { echo "$virsh_out"; exit "${E_libvirt}"; } return 0 } #==============================================================================# edit_xml () # DESCRIPTION: Edit and verify OPT[TEMPLATE]'s XML file # INPUT: None # OUTPUT: Status updates to user, error messages as needed # PARAMETERS: None #==============================================================================# { local xml="${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" [[ -e "$xml" ]] || { echo "The template XML is missing from $xml" echo "There is nothing to edit." exit "$E_template"; } >&2 check_template_exists || template_error $? local temp temp="$(mktemp)" || temp_error trap 'rm -f "$temp" &>/dev/null;' INT local ed_com="vi" [[ -n "$EDITOR" ]] && ed_com="$EDITOR" [[ -n "$VISUAL" ]] && ed_com="$VISUAL" local md5_curr md5_old md5_old="$(get_md5 "$xml")" copy_file "$xml" "$temp" local loop=1 while ((loop)); do "$ed_com" "$temp" || { echo "Editing XML with $ed_com failed" >&2; rm -f "$temp" &>/dev/null; exit "$E_extcom"; } md5_curr="$(get_md5 "$temp")" [[ "$md5_old" == "$md5_curr" ]] && { ((OPT[QUIET])) || echo "No changes made to original XML"; rm "$temp"; exit 0; } local virt_message="" if virt_message="$(virt-xml-validate "$temp" 2>&1)"; then loop=0 else echo >&2 echo "$virt_message" >&2 echo "Changes resulted in malformed XML file. " >&2 prompt_yes_abort \ "Press (e) to edit again, anything else to abort and revert" \ "^[Ee]$" >&2 || { rm "$temp" &>/dev/null; exit "$E_xml"; } fi done move_file "$temp" "$xml" check_template ((OPT[QUIET])) || echo "XML modified" trap 'exit' INT return 0 } #==============================================================================# get_format () # DESCRIPTION: Find format of given virtual machine image file # INPUT: Absolute filepath to a virtual machine image file # OUTPUT: Echoes the name of the format # PARAMETERS: $1: Filepath #==============================================================================# { local line level=0 while read -r line; do [[ "$line" =~ \{[[:space:]]*$ ]] && { ((level++)); continue; } [[ "$line" =~ \},?[[:space:]]*$ ]] && { ((level--)); continue; } if ((level == 1)); then [[ "$line" =~ \"format\":[[:space:]]*\"(.*)\" ]] && { echo "${BASH_REMATCH[1]}"; return 0; } fi done < <(qemu-img info --output=json "$1") return 1 } #==============================================================================# get_template_list () # DESCRIPTION: List existing templates, alphabetically ordered, one per # line # INPUT: None # OUTPUT: List of templates # PARAMETERS: None #==============================================================================# { sqlite3 "select name from TEMPLATES;" | sort return 0 } #==============================================================================# get_spice () # DESCRIPTION: Get the spice host and port of a running machine # INPUT: The machine number # OUTPUT: Echoes '$hostname $port' pertaining to spice connection # PARAMETERS: $1: The machine number #==============================================================================# { local uuid time line spice state time="$(date +%s)" uuid="${CL_MAP["$1"]}" until (( OPT[S_TIMEOUT] <= $(date +%s)-time )) ; do spice="$(virsh domdisplay --type spice "$uuid" 2>&1)" [[ "$spice" =~ ^'spice://'(.+):(.+)$ ]] || continue echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; return 0; done return 1 } #==============================================================================# get_state () # DESCRIPTION: Get the state of a machine by number # INPUT: The number of a clone managed by qq2clone # OUTPUT: Echoes the state of the machine # PARAMETERS: $1: Machine number #==============================================================================# { echo "${CL_STATE["$1"]}" return 0 } #==============================================================================# get_state_set () # DESCRIPTION: List all machines in a given state # INPUT: Name of a state # OUTPUT: Echoes a space delimited list of machine numbers (can be empty) # PARAMETERS: $1: state name #==============================================================================# { local id before=0 state while read -r id; do state="$(get_state "$id" )" if [[ "$state" == "$1" ]] || [[ "$1" == "all" ]]; then [[ "$id" == "0" ]] && continue ((before)) && echo -n " "; before=1 echo -n "$id " fi done < <(echo "${!CL_MAP[@]}" | tr " " "\n") echo return 0 } #==============================================================================# get_target_set () # DESCRIPTION: Get the set of all machines that a command may legally # operate on # INPUT: A command name # OUTPUT: Echoes a space delimited set of machine numbers (can be empty) # PARAMETERS: $1: The command name #==============================================================================# { local id before=0 statelist state statelist="$(list_states "$1")" while read -r id; do state="$(get_state "$id" )" if [[ "$statelist" =~ all ]] || [[ "$statelist" =~ $state ]] ; then [[ "$id" == "0" ]] && continue ((before)) && echo -n " "; before=1 echo -n "$id" fi done < <(echo "${!CL_MAP[@]}" | tr " " "\n") echo return 0 } #==============================================================================# list_display () # DESCRIPTION: Display list of clones and their state to user # INPUT: Optionally, pass argument "1" to print detailed xml list # OUTPUT: Clone list # PARAMETERS: $1: Set to one to print xml summary instead of a regular # list #==============================================================================# { declare -A statelist local line state if [[ -n "${CL_MAP[0]}" ]]; then statelist["${CL_STATE[0]}"]="0" fi while read -r line; do [[ -z "$line" ]] && continue state="$(get_state "$line")" if [[ -n "${statelist["$state"]}" ]]; then statelist["$state"]="${statelist["$state"]} $line" else statelist["$state"]="$line" fi done < <(get_state_set all | tr " " "\n") declare -a states machines read -ra states \ < <(echo "${!statelist[@]}" |tr " " "\n" | sort | tr "\n" " ") local state m if (($1)); then #shellcheck disable=2119 # This is only a (functioning) mock implementation meant as a proof of # concept for what XML describing qq2clone's current state may be like # For this feature to be complete, it would: use a defined format, be # implemented with proper, modular code, and contain all information to # fully define qq2clone's state except for machine images and domain xml. echo " " else echo "[${OPT[TEMPLATE]}]"; if [[ -z "${statelist[*]}" ]]; then echo " No clones" else for state in "${states[@]}"; do read -ra machines <<<"$(strip_ws "${statelist["$state"]}")" echo -n " ${state}: "; for elem_mach in "${machines[@]}"; do (( elem_mach )) || { echo -n "[#0: STAGING] "; continue; } echo -n "#$elem_mach " done echo done fi fi return 0 } #==============================================================================# list_states () # DESCRIPTION: Helper for get_target_set. Lists machine states that a # given command can act on. States are the same as listed in man virsh, # but "shut off" is "off", "in shutdown" is "in-shutdown", "saved" is # added, and "all" addresses machines in any state # INPUT: The command name # OUTPUT: Echoes a space delimited list of states # PARAMETERS: $1: The command name, or if no arguments list all states # as a space delimited list #==============================================================================# { if (($#)); then if [[ "$1" == connect ]]; then echo "all" elif [[ "$1" == destroy ]]; then echo "running idle paused in-shutdown pmsuspended" elif [[ "$1" == exec ]]; then echo "all" elif [[ "$1" == restore ]]; then echo "saved" elif [[ "$1" == resume ]]; then echo "paused" elif [[ "$1" == rm ]]; then echo "all" elif [[ "$1" == rm-wipe ]]; then echo "all" elif [[ "$1" == rm-shred ]]; then echo "all" elif [[ "$1" == save ]]; then echo "running pmsuspended idle paused paused" elif [[ "$1" == save-rm ]]; then echo "saved" elif [[ "$1" == start ]]; then echo "off crashed saved" elif [[ "$1" == suspend ]]; then echo "running pmsuspended idle" fi else echo -n "all crashed idle in-shutdown off paused pmsuspended running" echo " saved" fi return 0 } #==============================================================================# load_template () # DESCRIPTION: Run check_template, build global arrays CL_MAP[ID]=UUID, # CL_STATE[ID]=STATE, and BAD_CL[ID]=UUID # INPUT: None # OUTPUT: None # PARAMETERS: None #==============================================================================# { check_template unset BAD_CL CL_MAP CL_STATE NAME_MAP declare -ga BAD_CL CL_MAP CL_STATE NAME_MAP local t="${OPT[TEMPLATE]}" local check check="$(sqlite3 "select exists ( select id,uuid from CLONES where \ template='${OPT[TEMPLATE]}' );")" ((check)) || return 0 # Build array of all UUIDs -> IDs for template's clones declare -A uuid_map local id uuid while read -r id; do read -r uuid uuid_map["$uuid"]="$id" done < <(sqlite3 "select id,uuid from CLONES where template='$t';") lv_api_do_open lv_api_do_comm list while read -r uuid; do [[ -n "${uuid_map["$uuid"]}" ]] && CL_MAP["${uuid_map["$uuid"]}"]="$uuid" done < <(read_pipe); local match _uuid for uuid in "${!uuid_map[@]}"; do match=0 for _uuid in "${CL_MAP[@]}"; do [[ "$uuid" == "$_uuid" ]] && { match=1; break; } done ((match)) && continue BAD_CL["${uuid_map["$uuid"]}"]="$uuid" done local state="" name="" for id in "${!CL_MAP[@]}"; do uuid="${CL_MAP["$id"]}" lv_api_do_comm get_state "$uuid" && state="$(read_pipe)" CL_STATE["$id"]="$state" lv_api_do_comm get_name "$uuid" && name="$(read_pipe)" NAME_MAP["$id"]="$name" done lv_api_do_close return 0 } #==============================================================================# save_domain () # DESCRIPTION: Save the state of a machine to disk # INPUT: Machine number # OUTPUT: None # PARAMETERS: $1: Machine number #==============================================================================# { local uuid uuid="${CL_MAP["$1"]}" virsh managedsave "$uuid" &>/dev/null || { echo "Failed to save domain ${OPT[TEMPLATE]}#${1}"; echo "Does it have shared directories mounted?"; exit "$E_libvirt"; } >&2 return 0 } #==============================================================================# start_domain () # DESCRIPTION: If a domain is not running, start it # INPUT: VM number or "staging" # OUTPUT: None, except on error. Returns 0 if machine is running # PARAMETERS: $1: Machine number #==============================================================================# { local state uuid uuid="${CL_MAP["$1"]}" state="$(get_state "$1")" if [[ "$state" =~ ^(off|crashed|saved)$ ]]; then virsh start "$uuid" >/dev/null || { echo "Virsh failed to start domain" >&2; exit "$E_libvirt"; } elif [[ "$state" == "paused" ]]; then virsh resume "$uuid" >/dev/null || { echo "Virsh failed to resume domain" >&2; exit "$E_libvirt"; } fi return 0 } #==============================================================================# suspend_domain () # DESCRIPTION: Suspend execution state of machine # INPUT: A machine number # OUTPUT: None # PARAMETERS: $1: A machine number #==============================================================================# { local uuid uuid="${CL_MAP["$1"]}" virsh suspend "$uuid" &>/dev/null } #==============================================================================# unique_name_uuid () # DESCRIPTION: Generate a name and/or uuid unique within libvirt connection # INPUT: Choice of uuid, name or both, and base machine name if requesting # name # OUTPUT: Echo name and uuid in that order, on separate lines # PARAMETERS: $1: 0 for name only, 1 for uuid only, or 2 for both # $2: Base machine name (required unless $1 is 1) #==============================================================================# { # TODO there is no reason for this to be one function local name="$2" uuid loop line unique list if (($1 != 1)); then RANDOM=$$ list="$(virsh list --all --name)" unique=0 until ((unique)); do while read -r line; do [[ -z "$line" ]] && continue if [[ "$line" == "$name" ]]; then name="${2}___${RANDOM}" continue 2 fi done < <(echo "$list") unique=1 done echo "$name" fi if (($1 != 0)); then list="$(virsh list --all --uuid)" list="${list}$(echo;ls -1 "${OPT[STORAGE]}")" unique=0 uuid="$(uuidgen -r)" until ((unique)); do while read -r line; do [[ -z "$line" ]] && continue if [[ "$line" =~ .*"$uuid".* ]]; then uuid="$(uuidgen -r)" continue 2 fi done < <(echo "$list") unique=1 done echo "$uuid" fi return 0 } #---------------------------------------------# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---PARSE COMMAND STRING AND INVOKE COMMAND---# #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# #---------------------------------------------# # Generally, user input should be checked in this section. When functions # from other sections omit testing that user-defined values are valid, # this is why #==============================================================================# convert_to_seq () # DESCRIPTION: Take a set of the format described in the man page and echo # it as an ordered ascending, space delimited sequence of valid machine # numbers # INPUT: A set # OUTPUT: A sequence of numbers. Returns 1 if bad set # PARAMETERS: $1: A set #==============================================================================# { { [[ "$1" =~ ^[,-] ]] || [[ "$1" =~ [,-'^']$ ]] || [[ "$1" =~ [,-][,-] ]] || [[ "$1" =~ [[:space:]] ]] || [[ "$1" =~ ^.(.*[^',']\^) ]]; } && return 1 declare -a parts IFS="," read -ra parts <<<"$1" local states i p not LH RH minus plus states="$(list_states| tr " " "|")" for ((i=0;i<${#parts[@]};i++)); do p="${parts["$i"]}" not=0 if [[ "${p:0:1}" == "^" ]]; then p="${p:1}" not=1 fi if [[ "$p" =~ ^${states}$ ]]; then p="$(get_state_set "$p")" elif [[ "$p" =~ ^([0-9]+)'-'([0-9]+)$ ]]; then LH="${BASH_REMATCH[1]}"; RH="${BASH_REMATCH[2]}"; ((RH>LH)) || return 1 ((LH>0)) || return 1 for ((p=LH;LH0)) || return 1 else return 1 fi if ((not)); then minus="$minus $p" else plus="$plus $p" fi done [[ "$plus $minus" =~ [^0-9' '] ]] && return 1 local n before=0 while read -r n; do [[ -z "$n" ]] && continue [[ " $minus " =~ [[:space:]]${n}[[:space:]] ]] && continue ((before)) && echo -n " "; before=1 echo -n "$n" done < <( tr " " "\n" <<<"$plus" | sort -n | uniq ) echo return 0 } #==============================================================================# exec_com () # DESCRIPTION: Apply flags to modify script behavior, determine which # command the user has invoked, and perform some parsing and # validating of input until it's time to hand off to a more specific # function # INPUT: All positional parameters sent to qq2clone # OUTPUT: Error messages/exit values as needed # PARAMETERS: Pass "$@" directly here from the global scope #==============================================================================# { parse_flags "$@" local offset offset="$(read_pipe)" shift "$offset" local com com="$1" shift local verbose_coms verbose_coms="config|check|list|list-templates|exec|edit|modify-template" if (( OPT[QUIET] == 2)) && [[ ! "$com" =~ ^($verbose_coms)$ ]]; then exec &>/dev/null fi virsh uri |& grep -qi ^QEMU || { echo "Current libvirt URI is not QEMU:///" >&2; exit "$E_libvirt"; } [[ -n "$com" ]] || { usage >&2; exit "$E_args"; } # Commands which don't want check_template run (in advance of calling them # anyway) if [[ "$com" == "check" ]]; then exec_com_check "$@" exit 0 elif [[ "$com" == "copyright" ]]; then exec_com_copyright exit 0 elif [[ "$com" == "config" ]]; then exec_com_config "$@" exit 0 elif [[ "$com" == "copy-template" ]]; then exec_com_copy_template "$@" exit 0 elif [[ "$com" == "delete-template" ]]; then exec_com_delete_template "$@" exit 0 elif [[ "$com" == "import-template" ]]; then exec_com_import_template "$@" exit 0 elif [[ "$com" == "license" ]]; then exec_com_license exit 0 elif [[ "$com" == "list-templates" ]]; then exec_com_list_templates "$@" exit 0 elif [[ "$com" == "modify-template" ]]; then exec_com_modify_template "$@" exit 0 elif [[ "$com" == "edit" ]]; then exec_com_edit "$@" exit 0 elif [[ "$com" == "list" ]]; then exec_com_list "$@" exit 0 fi # Clone and the set commands below get check_template run if [[ "$com" == "clone" ]]; then check_template; template_error "$?" load_template exec_com_clone "$@" exit 0 fi # All remaining commands require similiar logic to invoke, and act on sets # of machines. set_coms[$com] provides a list of arguments to exec_com_set declare -A set_coms set_coms["connect"]="connect Connected" set_coms["destroy"]="destroy_domain Destroyed" set_coms["exec"]="This one is a special case, this text isn't used" set_coms["restore"]="start_domain Restored" set_coms["resume"]="start_domain Resumed" set_coms["rm"]="delete_machine Deleted 0" set_coms["rm-wipe"]="delete_machine Deleted_and_wiped 1" set_coms["rm-shred"]="delete_machine Deleted_and_shredded 2" set_coms["save"]="save_domain Saved" set_coms["save-rm"]="discard_save Discarded_saved_state 0" set_coms["start"]="start_domain Started" set_coms["suspend"]="suspend_domain Suspended" local match=0 elem for elem in "${!set_coms[@]}"; do [[ "$elem" == "$com" ]] && { match=1; break; } done ((match)) || { echo "Unknown command $com" >&2; exit "$E_args"; } (($#)) || arg_error 0 "$com" load_template local seq seq="$(convert_to_seq "$1")" || set_error shift local set set="$(seq_intersection "$seq" "$(get_target_set "$com")")" [[ -n "$set" ]] || target_error "$com" declare -a command_array read -ra command_array <<<"${set_coms["$com"]}" if [[ "$com" == exec ]]; then exec_com_exec "$set" "$@" exit 0 else (($#)) && arg_error 1 "$com" fi exec_com_set "$set" "${command_array[@]}" exit 0 } #==============================================================================# exec_com_check () # DESCRIPTION: Attempt to find and solve problems regarding # inconsistency between the database and UUIDs present on the libvirt # connection, files in qq2clone's pool that aren't associated with a # machine on the connection, or invalid template XML # INPUT: Specify a template name, or nothing to act on all templates # OUTPUT: Prompts and status updates # PARAMETERS: $1: (optional) template name #==============================================================================# { (($# > 1)) && arg_error 1 check local t declare -a templates if (($#)); then check_template_exists "$1" || template_error 1 templates[0]="$1" else while read -r t; do templates=( "${templates[@]}" "$t" ) done < <(get_template_list) fi for t in "${templates[@]}"; do echo "BEGIN CHECK TEMPLATE: $t" echo OPT[TEMPLATE]="$t" load_template if check_template; then echo "XML: Valid" else echo "XML: Invalid or missing" echo echo "Fix manually by repairing or replacing XML file at:" echo echo " ${OPT[TEMPLATE_DIR]}/${t}.xml" echo echo "qq2clone cannot repair bad XML. Fix this problem manually, or" echo "allow qq2clone to delete the template and all of its clones." echo echo "Delete $t and all of its clones?" echo "(Backing template disk files will not be touched)" echo if prompt_yes_no; then delete_template_and_clones echo hr continue fi fi echo local n sum sum="$(( ${#BAD_CL[@]} + ${#CL_MAP[@]} ))" echo "TOTAL CLONES: $sum" if (( ${#BAD_CL[@]} )); then echo echo "Of these clones, there are some that have either been deleted" echo "or undefined by a tool other than qq2clone, or exist on" echo "another libvirt URI and cannot presently be seen." echo echo "TOTAL MISSING CLONES: ${#BAD_CL[@]}" prompt_delete_bad_clones fi echo echo "END CHECK TEMPLATE: $t" echo hr echo done exit 0 } #==============================================================================# exec_com_clone () # DESCRIPTION: Create clones of OPT[TEMPLATE] # INPUT: A positive number, or nothing to create 1 clone # OUTPUT: Outputs a total at the end, error messages/exit codes as needed # PARAMETERS: $1: (Optional) a number #==============================================================================# { (( $# < 2 )) || arg_error 1 "clone" if (($#)); then [[ "$1" =~ ^[0-9]+$ ]] || arg_error 2 "clone" "$1" fi check_template_disks (( $# == 0 )) && { clone; exit 0; } local i for ((i=0;i<$1;i++)); do clone done exit 0 } #==============================================================================# exec_com_config () # DESCRIPTION: View or change configuration options # INPUT: subcommand and further arguments # OUTPUT: Varies # PARAMETERS: $1: Subcommand list, info, or edit, $2: option name (for # info and edit), $3: new option value (for edit) #==============================================================================# { if ((OPT[QUIET] == 2)); then [[ "$1" =~ ^(list|info)$ ]] || exec &>/dev/null fi (( $# )) || arg_error 0 config if [[ "$1" == "list" ]]; then [[ -n "$2" ]] && arg_error 1 "config list" disp_conf_names exit 0 fi local option read -r option < <(echo "$2" | tr "[:lower:]" "[:upper:]") if [[ "$1" == "info" ]]; then [[ -n "$2" ]] || arg_error 0 "config info" [[ -n "$3" ]] && arg_error 1 "config info" disp_conf_desc "$option" || exit "$E_args" exit 0 fi if [[ "$1" == "edit" ]]; then [[ -n "$2" ]] || arg_error 0 "config edit" [[ -n "$4" ]] && arg_error 1 "config edit" check_config "$option" || { echo "Unknown option: $option"; exit "$E_args"; } local line if (($#==3));then line="$3" else ((OPT[QUIET]==2)) && exit "$E_args" echo "Enter new value for $2 without quotes:" read -r line line="$(strip_ws "$line")" fi local retval write_config "$option" "$line" || retval="$?" (( retval == 1 )) && arg_error 2 "config edit" "$2" (( retval == 2 )) && { echo "Bad value for option $option: $line"; exit "$E_args"; } echo "Configuration value has been saved" exit 0 fi arg_error 2 config "$1" } #==============================================================================# exec_com_copy_template () # DESCRIPTION: Copy an existing template with a new name # INPUT: The old and new template name # OUTPUT: Status update/error messages # PARAMETERS: $1: Existing template, $2: Copy's name #==============================================================================# { (($# < 2)) && arg_error 0 copy-template (($# > 2)) && arg_error 1 copy-template local old new old="${OPT[TEMPLATE_DIR]}/$1.xml" new="${OPT[TEMPLATE_DIR]}/$2.xml" OPT[TEMPLATE]="$1" check_template || template_error "$?" template_name_available "$2" write_pipe 1 <"${old}" valid_xml_name_check "$2" do_virt_xml --edit --metadata name="$2" ((OPT[COPY_DISKS])) && copy_disks "$2" write_file "$new" local md5 md5="$(get_md5 "$new")" local disks get_disk_devices <"$new" disks="$(read_pipe)" sqlite3 "insert into TEMPLATES values ('$2','$md5','$disks','1');" (( OPT[QUIET] )) || echo "Copy \"$2\" of template \"$1\" made successfully" exit 0 } #==============================================================================# exec_com_copyright () # DESCRIPTION: Output copyright notice to user # INPUT: None # OUTPUT: Show copyright notice # PARAMETERS: None #==============================================================================# { echo "Copyright 2021, Jesse Gardner" echo echo "qq2clone and all files in this project are released under the terms" echo "of the GNU GPL v2. See the full copyright notice at the top of this" echo "file (the top of the qq2clone bash script)" echo echo "To read the full text of the GNU GPL v2 license, use the command:" echo " qq2clone license" return 0 } #==============================================================================# exec_com_delete_template () # DESCRIPTION: Delete an existing template # INPUT: A template name # OUTPUT: Status updates/error messages # PARAMETERS: $1: Template name #==============================================================================# { (( $# )) || arg_error 0 "delete-template" (( $# > 1 )) && arg_error 1 "delete-template" local txml="${OPT[TEMPLATE_DIR]}/${1}.xml" OPT[TEMPLATE]="$1" load_template check_template; template_error "$?" if [[ ! -e "$txml" ]]; then echo "No template named $1 exists" >&2 exit "$E_template" fi local char if ! ((OPT[QUIET])); then echo "Are you sure you want to delete template $1? Storage volumes" echo "will not be touched." prompt_yes_abort || exit 0 fi local check check="$(sqlite3 "select exists \ (select * from CLONES where template='$1');")" if ((check));then echo "Clones must be deleted before their parent template" echo "This can be done with:" echo echo " qq2clone -t ${OPT[TEMPLATE]} rm all" echo " qq2clone modify-template ${OPT[TEMPLATE]} discard-image" exit "$E_args" fi >&2; check_rw "$txml" rm -f "$txml" &>/dev/null || unexpected_error exec_com_delete_template sqlite3 "delete from TEMPLATES where name='$1';" ((OPT[QUIET])) || echo "Template $1 deleted" exit } #==============================================================================# exec_com_edit () # DESCRIPTION: Edit the XML of a clone # INPUT: A machine number # OUTPUT: Status messages/errors # PARAMETERS: $1: A machine number #==============================================================================# { virsh edit "${CL_MAP["$1"]}" exit 0 } #==============================================================================# exec_com_exec () # DESCRIPTION: Execute arbitrary commands in a context where $uuid, $name, # and $disks are defined # INPUT: A set of machines and a command string # OUTPUT: Dependent on input # PARAMETERS: $1: A set of machine numbers, $2 and on: arbitrary #==============================================================================# { declare -a machines read -ra machines <<<"$1" shift local elem uuid disks for elem in "${machines[@]}"; do uuid="${CL_MAP["$elem"]}" name="${NAME_MAP["$elem"]}" disks="$(sqlite3 "select disks from CLONES where id='$elem' and \ template='${OPT[TEMPLATE]}';")" export uuid name disks bash -ic "eval '$*'" || { ((OPT[QUIET]==2)) || echo "Iteration of exec failed, abort" >&2; exit "$E_args"; } done exit 0 } #==============================================================================# exec_com_import_template () # DESCRIPTION: Import xml from a libvirt domain, or from a file # INPUT: A filepath, or a libvirt domain name/uuid # OUTPUT: Status updates if not OPT["QUIET"], error messages if named # domain/xml does not exist # PARAMETERS: # $1: Absolute filepath to a libvirt domain XML file, or the name/uuid of # a domain defined on the current libvirt connection # $2: Optional. If set, use this value for template name rather than # the one in domain XML #==============================================================================# { (( $# < 1 )) && arg_error 0 import-template (( $# > 2 )) && arg_error 1 import-template local xml import_get_xml "$1" xml="$(read_pipe)" find_tag '//domain[@type="kvm"]/@type'<<<"$xml" local match match="$(read_pipe)" if [[ -z "$match" ]]; then find_tag '//domain[@type="qemu"]/@type'<<<"$xml" match="$(read_pipe)" if [[ -z "$match" ]]; then echo "Domain must be of type QEMU or KVM" >&2 exit "$E_template" fi fi write_pipe 1 <<<"$xml" get_template_name "$2" local xmlname name { read -r xmlname read -r name } < <(read_pipe) write_pipe 1 <<<"$xml" do_virt_xml --edit --metadata name="$name" ((OPT[COPY_DISKS])) && copy_disks "$name" write_file "${OPT[TEMPLATE_DIR]}/${name}.xml" get_disk_devices <"${OPT[TEMPLATE_DIR]}/${name}.xml" local disks disks="$(read_pipe)" local md5 md5="$(get_md5 "${OPT[TEMPLATE_DIR]}/${name}.xml")" sqlite3 "insert into TEMPLATES values ('$name','$md5','$disks','1');" ((OPT[QUIET])) || echo "Machine imported as: \"$name\"" if [[ "$1" =~ ^[^/] ]] && ! (( OPT[QUIET] )) ; then user_undefine_domain "$xmlname" fi exit 0 } #==============================================================================# exec_com_license () # DESCRIPTION: Output GNU GPL v2 license full text # INPUT: None # OUTPUT: Show license # PARAMETERS: None #==============================================================================# { echo "$archive" | base64 -d | tar -Ozx LICENSE return 0 } #==============================================================================# exec_com_list () # DESCRIPTION: List clones # INPUT: Nothing, "all", or "xml" # OUTPUT: A list of clones and their state # PARAMETERS: $1: (optional) "all" to run this command for all templates, # "xml" to produce an xml document with detailed information about # qq2clone's overall state #==============================================================================# { (( $# > 1)) && arg_error 1 "list" if (($#)); then local line if [[ "$1" == "all" ]]; then local before=0 while read -r line; do ((before)) && echo; before=1 OPT[TEMPLATE]="$line" load_template list_display 0 done < <(get_template_list) elif [[ "$1" == "xml" ]]; then echo "" local name value while read -r name; do read -r value echo " " done < <(sqlite3 "select name,value from CONFIG;") echo " ${LIBVIRT_DEFAULT_URI:-missing}" while read -r line; do OPT[TEMPLATE]="$line" load_template list_display 1 done < <(get_template_list) echo "" else arg_error 2 "list" "$1" fi else [[ -z "${OPT[TEMPLATE]}" ]] && { ((OPT[QUIET])) || echo "Specify the template to list" >&2 exit "$E_template"; } check_template_exists || template_error 1 load_template list_display fi exit 0 } #==============================================================================# exec_com_list_templates () # DESCRIPTION: List all template names # INPUT: None # OUTPUT: A list of template names # PARAMETERS: None #==============================================================================# { (($#)) && arg_error 1 list-templates get_template_list exit 0 } #==============================================================================# exec_com_modify_template () # DESCRIPTION: Modify image(s) of an existing template # INPUT: Template name, subcommand, further arguments # OUTPUT: Status updates/error messages # PARAMETERS: $1: Template name, $2 and on: subcommand and further # arguments, see man page or read the if/elifs below #==============================================================================# { if ((OPT[QUIET] == 2)); then [[ "$2" == "edit" ]] || exec &>/dev/null fi (($#<2)) && arg_error 0 modify-template (($#>3)) && arg_error 1 modify-template if (($#==3)); then { [[ "$2" == "commit-image" ]] || [[ "$2" == "rename" ]] ; } || arg_error 2 modify-template "$1" "$2" "$3" fi OPT[TEMPLATE]="$1" if [[ "$2" == "edit" ]]; then edit_xml exit 0 fi check_template; template_error "$?" load_template local is_staging=0 [[ -n "${CL_STATE[0]}" ]] && is_staging=1 [[ -n "${BAD_CL[0]}" ]] && is_staging=2 if [[ "$2" == "prepare-image" ]]; then OPT[NORUN]=1 ((is_staging == 2)) && stage_error ((is_staging)) || { clone 0; load_template; } connect 0 elif [[ "$2" == "commit-image" ]]; then ((is_staging == 2)) && stage_error if (($#==3)); then [[ "$3" == "force" ]] || arg_error 2 modify-template "$1" "$2" "$3" fi ((is_staging)) || { echo "No changes are staged" >&2; exit "$E_args"; } commit_image "$@" elif [[ "$2" == "destroy-image" ]]; then ((is_staging == 2)) && stage_error local state uuid state="$(get_state 0)" [[ "$state" == "running" ]] || { echo "Domain is not running" >&2; exit "$E_args"; } uuid="${CL_MAP[0]}" virsh destroy "$uuid" &>/dev/null elif [[ "$2" == "discard-image" ]]; then ((is_staging == 2)) && stage_error ((is_staging)) || { echo "No image to discard" >&2; exit "$E_args"; } delete_machine 0 0 ((OPT[QUIET])) || echo "Image discarded" elif [[ "$2" == rename ]]; then (( $#==3)) || arg_error 0 "modify-template $1 $2" rename_template "$@" else arg_error 2 "modify-template" "$2" fi exit 0 } #==============================================================================# exec_com_set () # DESCRIPTION: Any qq2clone command that acts on a set of machines, with # the exception of exec, uses this function to iterate over all machines # in the set # INPUT: Set of machines to act on, command to invoke, text to display to # user, and any additional parameters to pass on # OUTPUT: Status updates after each iteration # PARAMETERS: $1: the set to act on, $2: Function name, $3: Text to add # onto status update at end of function, with _ replaced by space, $4 # and on: passed on to function called #==============================================================================# { local set com text set="$1"; shift com="$1"; shift text="$1"; shift declare -a machines read -ra machines <<<"$set" while [[ "$text" =~ (.+)_+(.+) ]]; do text="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" done local elem for elem in "${machines[@]}"; do "$com" "$elem" "$@" ((OPT[QUIET])) || echo "${text}: ${OPT[TEMPLATE]}#${elem} ${CL_MAP["$elem"]}" done exit 0 } #==============================================================================# hr () # DESCRIPTION: Horizontal rule, constructed with - # INPUT: None # OUTPUT: Lots of - # PARAMETERS: None #==============================================================================# { echo ---------------------------------------------------------------------- return 0 } #==============================================================================# parse_flags () # DESCRIPTION: Check all flags and change global variables accordingly # INPUT: exec_com passes $@ here, containing all cmd line parameters # OUTPUT: write_pipe the number by which exec_com should shift, # or if unknown option is encountered exit with error message/code # PARAMETERS: $@: Parameters to exec_com, originally from command line #==============================================================================# { # Check for --quiter/-Q first declare -a args local d=0 args=( "$@" ) exec 8>&1 exec 9>&2 [[ "${args[0]}" == "--quieter" ]] && args[0]="-Q" if [[ "${args[0]}" =~ ^\-Q ]]; then OPT[QUIET]=2 exec &>/dev/null fi local short=":c:Cfghnqrs:St:vV" local long="connection=c,copy-disks=C,no-spice=f,use-spice=g,help=h," long="${long}no-run=n,quiet=q,run=r,storage=s,spicy=S," long="${long}template=t,verbose=v,virt-viewer=V" short_flags "$short" "$long" "${args[@]}" read -ra args < <(read_pipe) set -- "${args[@]}" local opt optstring while getopts "${short}Q" opt; do case "$opt" in c) LIBVIRT_DEFAULT_URI="$OPTARG" export LIBVIRT_DEFAULT_URI virsh list &>/dev/null || { echo "Virsh cannot connect to URI \"$OPTARG\", exiting" >&2; exit "$E_args"; } ;; C) OPT[COPY_DISKS]=1 ;; f) OPT[USE_SPICE]=0 ;; g) OPT[USE_SPICE]=1 ;; h) ((OPT[QUIET]==2)) && exit "$E_args" ((OPT[QUIET]=0)); usage; exit 0 ;; n) OPT[NORUN]=1 ;; q) (( OPT[QUIET] )) || OPT[QUIET]=1 ;; Q) : # Handled above ;; r) OPT[NORUN]=0 ;; s) storage_opt "$OPTARG" ;; S) OPT[USE_SPICE]=1; OPT[SPICY]=1 ;; t) OPT[TEMPLATE]="$OPTARG" check_template_exists; template_error $? ;; v) OPT[QUIET]=0 ;; V) OPT[USE_SPICE]=1; OPT[SPICY]=0 ;; :) [[ "$long" =~ (,|^)([^,]+)=$OPTARG ]] optstring="--${BASH_REMATCH[2]}/-${OPTARG}" echo "Required argument to $optstring not found" >&2 exit "$E_args" ;; *) echo "Option \"$OPTARG\" is not recognized" >&2 exit "$E_args" esac done ((OPTIND+=d)) { [[ -z "$OPTIND" ]] || (( OPTIND < 1 )) ; } && OPTIND=1 if [[ "${OPT[STORAGE]}/" =~ ^("${OPT[TEMPLATE_DIR]}/") ]]; then echo "Invalid storage location ${OPT[STORAGE]}" exit "$E_args" fi write_pipe 0 $(( OPTIND-1 )) exec 1>&8 8>&- exec 2>&9 9>&- return 0 } #==============================================================================# seq_intersection () # DESCRIPTION: Echo the intersection of two integer sets # INPUT: Two space delimited integer sets # OUTPUT: The intersection of the two sets, space delimited, ordered # ascending # PARAMETERS: $1, $2: Integer sets #==============================================================================# { local n before=0 while read -r n; do [[ -z "$n" ]] && continue [[ ! " $2 " =~ [[:space:]]${n}[[:space:]] ]] && continue ((before)) && echo -n " "; before=1 echo -n "$n" done < <(tr " " "\n" <<<"$1" | sort -n | uniq) echo return 0 } #==============================================================================# short_flags () # DESCRIPTION: translate long option names into short ones # INPUT: A string describing all short options, another mapping long # names to their short equivalents, and the arguments to check # OUTPUT: write_pipe the parameters $3 and on passed to this function, # but with long names translated to short ones # PARAMETERS: $1: a string of the type used by builtin getopts. Must # include ALL options, even those without a long name counterpart # $2: a string of the form a-long=a,b-long=b # $3 and on: all arguments to be checked #==============================================================================# { local s_string="$1" l_string="$2" shift 2 declare -a short_args=( "$@" ) local elem i for ((i=0;i<${#short_args[@]};i++)); do elem="${short_args["$i"]}" if [[ "$elem" == "--" ]]; then break elif [[ "$elem" =~ ^-- ]]; then elem="${elem:2}" if [[ "$l_string" =~ (^|,)($elem)=(.) ]]; then elem="${BASH_REMATCH[3]}" short_args["$i"]="-$elem" ((i--)) else echo "Unknown option: $elem" >&2 exit "$E_args" fi elif [[ "$elem" =~ ^- ]]; then local len=$(( ${#elem} - 1 )) elem="${elem:${len}}" if [[ "$s_string" =~ $elem: ]]; then ((i++)) fi else break fi done write_pipe 0 "${short_args[*]}" return 0 } #==============================================================================# storage_opt () # DESCRIPTION: Helper for parse_flags. Handles checking and setting user # option defining OPT[STORAGE] # INPUT: $OPTARG to -s option # OUTPUT: None, except on error # PARAMETERS: $1: argument to --storage/-s #==============================================================================# { if [[ "$1" =~ ^/ ]]; then OPT["STORAGE"]="$1" else virsh pool-info "$1" &>/dev/null || { echo "No such pool \"$1\" exists on current libvirt connection" >&2; exit "$E_args"; } local line match=0 virsh pool-dumpxml "$1" 2>/dev/null | find_tag "//pool[@type='dir']/target/path" while read -r line; do match=1 [[ "$line" =~ ^[\<\>] ]] || OPT[STORAGE]="$line" done < <(read_pipe) ((match)) || { echo "Specified pool is not of type dir, so it is not supported by" echo "qq2clone at this time" exit "$E_args"; } >&2 fi check_dir "${OPT[STORAGE]}" return 0 } #==============================================================================# strip_ws () # DESCRIPTION: Strip any leading and trailing whitespace # INPUT: A string # OUTPUT: Echo the result # PARAMETERS: $1: String to strip #==============================================================================# { local str="$1" if [[ "$str" =~ ^[[:space:]]+([^[:space:]].*)$ ]]; then str="${BASH_REMATCH[1]}" fi if [[ "$str" =~ ^(.*[^[:space:]])[[:space:]]+$ ]]; then str="${BASH_REMATCH[1]}" fi echo "$str" return 0 } #-------------# #@@@@@@@@@@@@@# #---ARCHIVE---# #@@@@@@@@@@@@@# #-------------# # This section contains a base64 encoded archive added in with # gen_all.bash. The last remaining section containing Bash scripting is # ENTRY POINT, at the bottom of this file archive=' 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 AAA= ' #-----------------# #@@@@@@@@@@@@@@@@@# #---ENTRY POINT---# #@@@@@@@@@@@@@@@@@# #-----------------# if ! ((QQ2_NOEXECUTE)); then check_depends #Ensure needed fds are not in use exec 3>&- 3<&- 4>&- 4<&-; lv_api_do_check open_pipe if [[ -n "$QQ2_DIR" ]]; then : # If already set, use existing value elif [[ -e "${HOME:?}/.config/qq2clone" ]]; then QQ2_DIR="$(<"${HOME}/.config/qq2clone")" else QQ2_DIR="${HOME}/storage-qq2clone" fi [[ "$1" == "setup" ]] && { first_run_setup; exit $?; } get_config exec_com "$@" exit 0 fi