3460 lines
118 KiB
Bash
Executable File
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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
|
|
AA==
|
|
'
|
|
|
|
#-----------------#
|
|
#@@@@@@@@@@@@@@@@@#
|
|
#---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
|