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