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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
|
|
oAAA
|
|
'
|
|
|
|
#-----------------#
|
|
#@@@@@@@@@@@@@@@@@#
|
|
#---ENTRY POINT---#
|
|
#@@@@@@@@@@@@@@@@@#
|
|
#-----------------#
|
|
|
|
if ! ((QQ2_NOEXECUTE)); then
|
|
check_depends
|
|
|
|
#Ensure needed fds are not in use
|
|
exec 3>&- 3<&- 4>&- 4<&-;
|
|
|
|
lv_api_do_check
|
|
open_pipe
|
|
if [[ -n "$QQ2_DIR" ]]; then
|
|
: # If already set, use existing value
|
|
elif [[ -e "${HOME:?}/.config/qq2clone" ]]; then
|
|
QQ2_DIR="$(<"${HOME}/.config/qq2clone")"
|
|
else
|
|
QQ2_DIR="${HOME}/storage-qq2clone"
|
|
fi
|
|
|
|
[[ "$1" == "setup" ]] && { first_run_setup; exit $?; }
|
|
|
|
get_config
|
|
exec_com "$@"
|
|
exit 0
|
|
fi
|