3470 lines
118 KiB
Bash
Executable File
3470 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"; }
|
|
if ((${#spice[@]} > 1)); then
|
|
nohup spicy --title "${OPT[TEMPLATE]}#$1" -h "${spice[0]}" \
|
|
-p "${spice[1]}" &>/dev/null &
|
|
else
|
|
nohup spicy --title "${OPT[TEMPLATE]}#$1" \
|
|
--uri="$spice" &>/dev/null &
|
|
fi
|
|
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)"
|
|
if [[ "$spice" =~ ^'spice://'(.+):(.+)$ ]]; then
|
|
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
|
|
elif [[ "$spice" =~ ^'spice+unix' ]]; then
|
|
echo "$spice"
|
|
else
|
|
continue
|
|
fi
|
|
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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
|
|
FACgAAA=
|
|
'
|
|
|
|
#-----------------#
|
|
#@@@@@@@@@@@@@@@@@#
|
|
#---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
|