3187 lines
		
	
	
		
			98 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			3187 lines
		
	
	
		
			98 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
| #!/bin/bash
 | |
| #shellcheck disable=1090 disable=2012
 | |
| 
 | |
|                         #--------------------#
 | |
|                         #@@@@@@@@@@@@@@@@@@@@#
 | |
|                         #---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"
 | |
|   echo "$archive" | base64 -d | tar -zx
 | |
| }
 | |
| #=========================================================================#
 | |
| 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 ! { 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
 | |
|     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
 | |
| 
 | |
| 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 delete-template"
 | |
| echo "            destroy edit exec import-template list list-templates"
 | |
| echo "            modify-template restore resume rm rm-wipe rm-shred save"
 | |
| echo "            save-rm start 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" == "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" == "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_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_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
 | |
|   [[ "$1" == "setup" ]] && { first_run_setup; exit $?; }
 | |
| 
 | |
|   #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
 | |
|   get_config
 | |
|   exec_com "$@"
 | |
|   exit 0
 | |
| fi
 |