qq2clone/qq2clone

3460 lines
118 KiB
Bash
Executable File

#!/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 <https://www.gnu.org/licenses/>. #
#------------------------------------------------------------------------#
#--------------------#
#@@@@@@@@@@@@@@@@@@@@#
#---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 <<EOF
create table CLONES(uuid TEXT, id INTEGER, template TEXT, disks TEXT);
create table TEMPLATES(name TEXT, md5sum TEXT, disks TEXT,\
valid INTEGER);
create table CONFIG(name TEXT, value TEXT);
insert into CONFIG values('TEMPLATE_DIR', '${TEMPLATE_DIR}');
insert into CONFIG values('USE_SPICE', '${use_spice}');
insert into CONFIG values('SPICY', '${spicy}');
insert into CONFIG values('QUIET', '0');
insert into CONFIG values('S_TIMEOUT', '10');
insert into CONFIG values('STORAGE', '${POOL_DIR}');
insert into CONFIG values('TEMPLATE', '');
insert into CONFIG values('NORUN', '0');
EOF
echo "Setup complete"
return 0
}
#==============================================================================#
get_config ()
# DESCRIPTION: Load configuration from database
# INPUT: None
# OUTPUT: Silent except on error
# PARAMETERS: None
#==============================================================================#
{
declare -g QQ2_DIR
QQ2_DIR="$(<"${HOME}/.config/qq2clone")"
QQ2_DIR="$(strip_ws "$QQ2_DIR")"
local check
read -r check \
< <(sqlite3 "select exists ( select * from CONFIG)")
((check)) ||
{ echo "Is the database corrupt? No CONFIG table!";
exit "$E_unexpected"; } >&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 " <template name=\"${OPT[TEMPLATE]}\">"
local disk
get_disk_devices_db
while read -r disk; do
echo " <disk>${disk}</disk>"
done < <(read_pipe)
echo " <xml>${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml</xml>"
local uuid
for state in "${states[@]}"; do
echo " <cloneset state=\"$state\">"
read -ra machines <<<"${statelist["$state"]}"
for m in "${machines[@]}" ; do
echo -n " <clone id=\"$m\" "
echo -n "name=\"${NAME_MAP["$m"]}\" "
uuid="${CL_MAP["$m"]}"
echo "uuid=\"$uuid\">"
get_disk_devices_db "$m"
while read -r disk; do
echo " <disk>${disk}</disk>"
done < <(read_pipe)
echo " </clone>"
done
echo " </cloneset>"
done
local id
if ((${#BAD_CL[@]})); then
echo " <cloneset state=\"missing\">"
for id in "${!BAD_CL[@]}"; do
echo " <clone id=\"$id\" uuid=\"${BAD_CL["$id"]}\">"
get_disk_devices_db "$id"
while read -r disk; do
echo " <disk>${disk}</disk>"
done < <(read_pipe)
echo " </clone>"
done
echo " </cloneset>"
fi
echo " </template>"
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;LH<RH;LH++)); do
p="$p $((LH+1))"
done
elif [[ "$p" =~ ^[0-9]+$ ]]; then
((p>0)) || 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 "<qq2clone directory=\"${QQ2_DIR}\">"
local name value
while read -r name; do
read -r value
echo " <config name=\"$name\" value=\"$value\" />"
done < <(sqlite3 "select name,value from CONFIG;")
echo " <URI>${LIBVIRT_DEFAULT_URI:-missing}</URI>"
while read -r line; do
OPT[TEMPLATE]="$line"
load_template
list_display 1
done < <(get_template_list)
echo "</qq2clone>"
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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
oAAA
'
#-----------------#
#@@@@@@@@@@@@@@@@@#
#---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