|
@@ -1,1139 +0,0 @@
|
|
|
-#!/bin/bash
|
|
|
-# This script manages xenguest
|
|
|
-#
|
|
|
-set -u
|
|
|
-this="$0"
|
|
|
-
|
|
|
-XENGUEST_CONF_BASE="/etc/xenguest"
|
|
|
-LOGFILE="/var/log/xenguest"
|
|
|
-
|
|
|
-# Valid values for log level
|
|
|
-LOG_LEVEL_VALID="FATAL ERROR INFO VERBOSE"
|
|
|
-
|
|
|
-# Log levels being written to logfile
|
|
|
-LOG_LEVEL_LIST="ERROR INFO VERBOSE"
|
|
|
-# Affected by -v(v) param and conf file
|
|
|
-
|
|
|
-# Log levels being written to terminal
|
|
|
-VERBOSE_LOG_LEVEL="ERROR"
|
|
|
-# Constant
|
|
|
-
|
|
|
-# Highest Log Level: Default is ERROR only
|
|
|
-LOG_LEVEL="ERROR"
|
|
|
-# Used to update LOG_LEVEL_LIST
|
|
|
-
|
|
|
-
|
|
|
-# This should only be called from either log() or log_command.
|
|
|
-# It expectd $loglevel and $text to already be in scope
|
|
|
-function log_to_file ()
|
|
|
-{
|
|
|
- if [[ ${LOG_LEVEL_LIST} = *${loglevel}* ]]; then
|
|
|
- tstamp="$(date +"%d-%m-%Y %T")"
|
|
|
- tag="[${loglevel}]"
|
|
|
-
|
|
|
- printf "%s %-9s %s\n" "$tstamp" "$tag" "$text" >> ${LOGFILE}
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Write a log to the logfile, and to the console
|
|
|
-# Messages are written to the log with the date and a timestamp
|
|
|
-function log ()
|
|
|
-{
|
|
|
- # Inputs:
|
|
|
- # $1 - optional level to log at, one of ${LOG_LEVEL_VALID}
|
|
|
- # Default: INFO
|
|
|
- # $@ - log message body
|
|
|
-
|
|
|
- # get loglevel from parameter and capitalise
|
|
|
- loglevel=${1^^}
|
|
|
-
|
|
|
- # If no loglevel is passed use INFO
|
|
|
- if [[ ${LOG_LEVEL_VALID} = *${loglevel:-INVALID}* ]]; then
|
|
|
- shift
|
|
|
- else
|
|
|
- loglevel="INFO"
|
|
|
- fi
|
|
|
-
|
|
|
- # Kill script immediately after a fatal log
|
|
|
- killscript=0
|
|
|
- if [ "FATAL" = ${loglevel} ]; then
|
|
|
- killscript=1
|
|
|
- # Log at error level for the user
|
|
|
- loglevel="ERROR"
|
|
|
- fi
|
|
|
-
|
|
|
- text="$*"
|
|
|
- log_to_file
|
|
|
-
|
|
|
- # Write to terminal if level is high enough
|
|
|
- if [[ ${VERBOSE_LOG_LEVEL} = *${loglevel}* ]]; then
|
|
|
- echo "${loglevel}: ${text}"
|
|
|
- fi
|
|
|
-
|
|
|
- # if Log was fatal, kill the script
|
|
|
- if [[ ${killscript} = 1 ]]; then
|
|
|
- exit 1
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Write a shell command to the log and execute it
|
|
|
-# The stdout and stderr output of the command is captured in a variable,
|
|
|
-# and written to the logfile in two cases:
|
|
|
-# 1. The script is in verbose mode
|
|
|
-# 2. The command returns a non-zero status AND
|
|
|
-# The loglevel parameter (default: ERROR) is in $LOG_LEVEL_LIST
|
|
|
-#
|
|
|
-# This means by default a non-zero status results in a log tagged [ERROR],
|
|
|
-# but if a command is expected to fail, the tag can be reduced for visual
|
|
|
-# clarity
|
|
|
-log_command ()
|
|
|
-{
|
|
|
- # Inputs:
|
|
|
- # $1 - optional level to write errors at, one of ${LOG_LEVEL_VALID}
|
|
|
- # Default: ERROR
|
|
|
- # $@ - command to execute
|
|
|
-
|
|
|
- # get loglevel from parameter and capitalise
|
|
|
- loglevel=${1^^}
|
|
|
-
|
|
|
- # If no level passed, log output on failure at ERROR
|
|
|
- if [[ ${LOG_LEVEL_VALID} = *"${loglevel:-INVALID}"* ]]; then
|
|
|
- shift
|
|
|
- else
|
|
|
- loglevel="ERROR"
|
|
|
- fi
|
|
|
-
|
|
|
- # Commands cannot be logged at FATAL.
|
|
|
- if [ "FATAL" = ${loglevel} ]; then
|
|
|
- loglevel="ERROR"
|
|
|
- fi
|
|
|
- local command="$*"
|
|
|
- local output=""
|
|
|
- local status=0
|
|
|
-
|
|
|
- # Capture stdout and sterr to write to logfile
|
|
|
- output=$(eval "${command} 2>&1")
|
|
|
- status=$?
|
|
|
- # If command failed, or verbose mode, write log
|
|
|
- if [[ ${status} -ne 0 ]] || [[ ${LOG_LEVEL_LIST} = *VERBOSE* ]]; then
|
|
|
-
|
|
|
- # if command didn't fail write it at verbose level
|
|
|
- if [[ ${status} -eq 0 ]]; then
|
|
|
- loglevel="VERBOSE"
|
|
|
- fi
|
|
|
- # otherwise write it at ${loglevel} from arguments
|
|
|
-
|
|
|
- local append_to="/dev/null"
|
|
|
- # If we are writing ${loglevel} logs to file, use file as append_to
|
|
|
- if [[ ${LOG_LEVEL_LIST} = *${loglevel}* ]]; then
|
|
|
- append_to=${LOGFILE}
|
|
|
- fi
|
|
|
-
|
|
|
- # Log that command was called
|
|
|
- text="> ${command}"
|
|
|
- log_to_file
|
|
|
-
|
|
|
- # Write command output to logfile or /dev/null, indent to match rest of logs
|
|
|
- if [[ -n ${output} ]]; then
|
|
|
- echo "${output}" | sed 's/^/ /' >> ${append_to}
|
|
|
- fi
|
|
|
- # Log exit status
|
|
|
- text="< Exited with status ${status}"
|
|
|
- log_to_file
|
|
|
- fi
|
|
|
- # Ensure return status is captured
|
|
|
- return $status
|
|
|
-}
|
|
|
-
|
|
|
-# Sources a shell script and logs it
|
|
|
-log_source ()
|
|
|
-{
|
|
|
- local script=${1}
|
|
|
- log verbose "> source ${script}"
|
|
|
-
|
|
|
- ( . ${script} )
|
|
|
-
|
|
|
- status=$?
|
|
|
- log verbose "< Exited with status ${status}"
|
|
|
-
|
|
|
- return $status
|
|
|
-}
|
|
|
-
|
|
|
-if [ ! -f ${XENGUEST_CONF_BASE}/xenguest-manager.conf ]; then
|
|
|
- log fatal "Cannot find xenguest manager configuration"
|
|
|
-fi
|
|
|
-
|
|
|
-# Following variables must be set in configuration:
|
|
|
-# XENGUEST_VOLUME_DEVICE: device to use for lvm
|
|
|
-# XENGUEST_VOLUME_NAME: lvm volume name to create on device
|
|
|
-# Optionally set:
|
|
|
-# XENGUEST_LOG_LEVEL: the loglevel for terminal and logfile
|
|
|
-source ${XENGUEST_CONF_BASE}/xenguest-manager.conf
|
|
|
-
|
|
|
-# Check that VERBOSE level from config file is valid
|
|
|
-if [[ ${LOG_LEVEL_LIST} = *${XENGUEST_LOG_LEVEL}* ]]; then
|
|
|
- LOG_LEVEL=${XENGUEST_LOG_LEVEL}
|
|
|
-else
|
|
|
- log error "Invalid log level '${XENGUEST_LOG_LEVEL}' found in xenguest-manager.conf"
|
|
|
-fi
|
|
|
-
|
|
|
-function usage() {
|
|
|
- cat <<EOF
|
|
|
-Usage $this [-v(v)] ACTION [OPTIONS]
|
|
|
-
|
|
|
-with ACTION being one of:
|
|
|
- help
|
|
|
- Display this help
|
|
|
-
|
|
|
- create GUESTFILE [GUESTNAME]
|
|
|
- Create a guest using xenguest image GUESTFILE and name it GUESTNAME.
|
|
|
- This will extract and configure the guest and will also create the guest
|
|
|
- disk if guest has one configured.
|
|
|
- GUESTNAME is set to the basename of GUESTFILE if unspecified.
|
|
|
- GUESTNAME guest must not exist
|
|
|
-
|
|
|
- remove GUESTNAME
|
|
|
- Remove GUESTNAME and destroy its disk (if it has one)
|
|
|
-
|
|
|
- start GUESTNAME
|
|
|
- Start guest GUESTNAME
|
|
|
-
|
|
|
- stop|shutdown GUESTNAME
|
|
|
- Stop guest GUESTNAME (send stop signal and let it shutdown normally)
|
|
|
- Pass 'stop|shutdown GUESTNAME --nowait' to return immediately, rather
|
|
|
- than waiting for success or failure to return.
|
|
|
- Pass 'stop|shutdown GUESTNAME --kill' to force kill the guest if
|
|
|
- signalling the graceful shutdown fails for any reason
|
|
|
-
|
|
|
- These two parameters are incompatible, so only one should be passed
|
|
|
-
|
|
|
- kill|destroy GUESTNAME
|
|
|
- Kill guest GUESTNAME (stop directly the guest without signaling it)
|
|
|
-
|
|
|
- list
|
|
|
- List configured guests
|
|
|
-
|
|
|
- status
|
|
|
- List guests and their current status (running or stopped)
|
|
|
-
|
|
|
-Passing -v will enable INFO logs, and -vv will enable VERBOSE and INFO logs.
|
|
|
-Both increase what is written to ${LOGFILE},
|
|
|
-rather than the terminal.
|
|
|
-EOF
|
|
|
-}
|
|
|
-
|
|
|
-# Ensure init scripts in subshells do not call private functions
|
|
|
-function check_private()
|
|
|
-{
|
|
|
-
|
|
|
- # Return:
|
|
|
- # 0 - success
|
|
|
- # 1 - failure
|
|
|
-
|
|
|
- if [ $BASH_SUBSHELL -ne 0 ]; then
|
|
|
- log fatal "Attempted to execute private function '${FUNCNAME[1]}()' in a subshell!"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-is_integer() {
|
|
|
-
|
|
|
- if ! [[ "${1}" =~ ^[0-9]+$ ]]; then
|
|
|
- log fatal "invalid number '${1}'"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-# check size and convert it to MB, e.g '1[G]' => '1000M'
|
|
|
-check_size() {
|
|
|
- local disksize="${1}"
|
|
|
-
|
|
|
- [ -n "${disksize}" ] || disksize="invalid"
|
|
|
-
|
|
|
- # disksize may have appended M or G suffix, let's extract it
|
|
|
- # ${var:offset:length}, where #var is var length
|
|
|
- local lastchar="${disksize:${#disksize}-1}"
|
|
|
- case "${lastchar}" in
|
|
|
- [0-9])
|
|
|
- # backwards compatibility
|
|
|
- is_integer "${disksize}"
|
|
|
- echo -e "$((${disksize} * 1000))M"
|
|
|
- return
|
|
|
- ;;
|
|
|
- G|M)
|
|
|
- if [ "${#disksize}" -gt "1" ]; then
|
|
|
- local size="${disksize::${#disksize}-1}"
|
|
|
- is_integer "${size}"
|
|
|
- # convert GB to MB
|
|
|
- [ "${lastchar}" = "M" ] || size=$((${size} * 1000))
|
|
|
- echo -e "${size}M"
|
|
|
- return
|
|
|
- fi
|
|
|
- ;;
|
|
|
- *)
|
|
|
- ;;
|
|
|
- esac
|
|
|
-
|
|
|
- log fatal "Invalid size format '${1}'. Supported size format is e.g 1000M or 2[G]"
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_volume_init()
|
|
|
-{
|
|
|
- # Inputs:
|
|
|
- # $1 - diskdevice
|
|
|
- # $2 - volumename
|
|
|
- #
|
|
|
- # Outputs:
|
|
|
- # 0 - success
|
|
|
- # 1 - failure
|
|
|
-
|
|
|
- local diskdevice
|
|
|
- local volumename
|
|
|
-
|
|
|
- diskdevice="${1}"
|
|
|
- volumename="${2}"
|
|
|
-
|
|
|
- log info "Attempting to initialise xenguest volume '${volumename}'"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- if [ -z "${diskdevice}" -o ! -b "${diskdevice}" ]; then
|
|
|
- log error "Invalid volume device in configuration: '${diskdevice}'"
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- if [ -z "${volumename}" ]; then
|
|
|
- log error "Invalid volume name in configuration: '${volumename}'"
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- log_command verbose "pvs ${diskdevice}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- # Check if there is no filesystem in the block device
|
|
|
- log verbose "Checking for existing filesystem"
|
|
|
- filesystem=$(lsblk -n -o FSTYPE ${diskdevice})
|
|
|
- if [[ $? -eq 0 && -z "$filesystem" ]]; then
|
|
|
- log verbose "No filesystem found"
|
|
|
- log info "Initializing lvm on ${diskdevice}"
|
|
|
- log_command "pvcreate -f ${diskdevice}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Initialing lvm on ${diskdevice} failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
- else
|
|
|
- [ -z "$filesystem" ] || \
|
|
|
- log error "${diskdevice} is already formatted as $filesystem."
|
|
|
- return 1
|
|
|
- fi
|
|
|
- fi
|
|
|
-
|
|
|
- log_command verbose "vgs ${volumename}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log info "Creating ${volumename} volume"
|
|
|
- log_command "vgcreate ${volumename} ${diskdevice}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Creating ${volumename} volume failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
- fi
|
|
|
-
|
|
|
- log info "xenguest volume '${volumename}' initialised successfully"
|
|
|
-
|
|
|
- return 0
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-# Detach a disk we attached to xen
|
|
|
-function xenguest_detach_disk()
|
|
|
-{
|
|
|
- log verbose "Attempting to detach partition '${part}'"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- log_command "xl block-detach 0 \$(xl block-list 0 | grep \"domain/0\" | awk '{print \$1}')"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Detaching partition '${part}' failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- log verbose "Partition '${part}' detached successfully"
|
|
|
-}
|
|
|
-
|
|
|
-#Private
|
|
|
-function xenguest_volume_remove()
|
|
|
-{
|
|
|
- # Inputs:
|
|
|
- # $1 - volumename
|
|
|
- # $2 - guestname
|
|
|
-
|
|
|
- local volumename
|
|
|
- local guestname
|
|
|
-
|
|
|
- volumename="${1}"
|
|
|
- guestname="${2}"
|
|
|
-
|
|
|
- devname="/dev/${volumename}/${guestname}"
|
|
|
-
|
|
|
- # Remove volume if it exists
|
|
|
- log verbose "Checking for volume ${devname}"
|
|
|
- log_command verbose "lvs ${volumename}/${guestname}"
|
|
|
- if [ $? -eq 0 ]; then
|
|
|
- log info "Removing volume ${devname}"
|
|
|
- log_command "lvremove -y ${devname}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Removing volume ${devname} failed."
|
|
|
- return 1
|
|
|
- else
|
|
|
- log verbose "Volume ${devname} removed successfully"
|
|
|
- return 0
|
|
|
- fi
|
|
|
- fi
|
|
|
-
|
|
|
- log verbose "Volume ${devname} not found"
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_disk_init()
|
|
|
-{
|
|
|
- # Inputs:
|
|
|
- # $1 - guestname
|
|
|
- # $2 - guestfile
|
|
|
- #
|
|
|
- # Outputs:
|
|
|
- # 0 - success
|
|
|
- # 1 - failed at guest disk preparation
|
|
|
- # 2 - failed at guest disk creation
|
|
|
-
|
|
|
- guestname="$1"
|
|
|
- guestfile="$2"
|
|
|
-
|
|
|
- log info "Attempting to initialise disk for guest '${guestname}'"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- source ${XENGUEST_CONF_BASE}/guests/${guestname}/disk.cfg
|
|
|
- if [ -z "${DISK_DEVICE}" ]; then
|
|
|
- log info "Using disk device and volume name from xenguest-manager.conf"
|
|
|
- # By default guest is using disk defined inside xenguest-manager.conf
|
|
|
- diskdevice="${XENGUEST_VOLUME_DEVICE}"
|
|
|
- volumename="${XENGUEST_VOLUME_NAME}"
|
|
|
- else
|
|
|
- log info "Using disk device set in disk.cfg"
|
|
|
- # If guest configuration contains custom disk setting,
|
|
|
- # overwrite default one
|
|
|
- diskdevice="${DISK_DEVICE}"
|
|
|
- volumename="vg-xen-$(basename ${diskdevice})"
|
|
|
- fi
|
|
|
-
|
|
|
- log verbose "Disk Device = ${diskdevice}"
|
|
|
- log verbose "Volume Name = ${volumename}"
|
|
|
-
|
|
|
- devname="/dev/${volumename}/${guestname}"
|
|
|
-
|
|
|
- DISK_SIZE=$(check_size "${DISK_SIZE}")
|
|
|
- if [ -z "${DISK_SIZE}" ] || [ "${DISK_SIZE}" = "0M" ]; then
|
|
|
- log info "No disk for ${guestname}"
|
|
|
- return
|
|
|
- fi
|
|
|
-
|
|
|
- # Init our volume
|
|
|
- xenguest_volume_init "${diskdevice}" "${volumename}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- log info "Creating hard drive for guest '${guestname}'. This might take a while..."
|
|
|
-
|
|
|
- # Remove volume if it already exist
|
|
|
- xenguest_volume_remove ${volumename} ${guestname}
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- # Create volume
|
|
|
- log info "Creating volume '${volumename}/${guestname}'"
|
|
|
- log_command "lvcreate -y -L ${DISK_SIZE} -n ${guestname} ${volumename}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Creating volume '${volumename}/${guestname}' failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- # Add partition table
|
|
|
- log verbose "Creating partition table on ${devname}"
|
|
|
- log_command "parted -s \"${devname}\" mklabel msdos"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Creating partition table on ${devname} failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- # Setup disk name in xen configuration
|
|
|
- log verbose "Setting disk name in xen configuration"
|
|
|
- log_command "xenguest-mkimage update \"${XENGUEST_CONF_BASE}/guests/${guestname}\" --xen-disk=\"${devname}\""
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Setting disk name in xen configuration failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- # Create partitions
|
|
|
- partstart="0"
|
|
|
-
|
|
|
- # For each partition X the disk.cfg file should set a variable DISK_PARTX
|
|
|
- # with a : separated list defining the partition:
|
|
|
- # DISK_PART3="4:ext4:disk.tgz" means that partition 3 should be 4G formated
|
|
|
- # with ext4 and initialized with the content of disk.tgz
|
|
|
- # Keep user defined partition order,
|
|
|
- # even if previous partitions are not defined.
|
|
|
- # Create 2MB partitions in this case
|
|
|
- lastpart="0"
|
|
|
- for partidx in $(seq 1 4); do
|
|
|
- local _part="DISK_PART${partidx}"
|
|
|
- if [ -n "${!_part:=}" ]; then
|
|
|
- lastpart="${partidx}"
|
|
|
- fi
|
|
|
- done
|
|
|
-
|
|
|
- if [ "${lastpart}" -eq "0" ]; then
|
|
|
- # Nothing to be added here
|
|
|
- # No partition definition found
|
|
|
- return
|
|
|
- fi
|
|
|
-
|
|
|
- for part in $(seq 1 "${lastpart}"); do
|
|
|
- eval partdesc="\${DISK_PART${part}:=}"
|
|
|
- size=$(echo ${partdesc} | sed -e "s/\(.*\):.*:.*/\1/")
|
|
|
- fstype=$(echo ${partdesc} | sed -e "s/.*:\(.*\):.*/\1/")
|
|
|
- content=$(echo ${partdesc} | sed -e "s/.*:.*:\(.*\)/\1/")
|
|
|
-
|
|
|
- local _part="DISK_PART${part}"
|
|
|
- [ -n "${!_part:=}" ] || size="2M"
|
|
|
-
|
|
|
- size=$(check_size "${size}")
|
|
|
- if [ -n "${size}" ] && [ "${size}" != "0M" ]; then
|
|
|
- # size has appended M or G suffix, let's extract just the value
|
|
|
- # ${var:offset:length}, where #var is var length
|
|
|
- size="${size::${#size}-1}"
|
|
|
- partend=$(expr ${partstart} + ${size})
|
|
|
-
|
|
|
- # Let first MB of disk free for partition table
|
|
|
- if [ ${partstart} -eq 0 ]; then
|
|
|
- partstart="1"
|
|
|
- fi
|
|
|
-
|
|
|
- # Create partition
|
|
|
- log verbose "Adding partition ${part}"
|
|
|
- log_command "parted -s \"${devname}\" unit MB mkpart primary \"${partstart}\" \"${partend}\""
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Adding partition ${part} failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
- # Set next partition start to current partition end
|
|
|
- partstart="${partend}"
|
|
|
-
|
|
|
- # Sync to see the created partition
|
|
|
- log verbose "Sync created partition"
|
|
|
- log_command "sync"
|
|
|
-
|
|
|
- # Prepare format command
|
|
|
- if [ -n "${fstype}" ]; then
|
|
|
- case ${fstype} in
|
|
|
- vfat|ext2|ext3|ext4)
|
|
|
- formatcmd="mkfs.${fstype} -F"
|
|
|
- ;;
|
|
|
- swap)
|
|
|
- formatcmd="mkswap"
|
|
|
- ;;
|
|
|
- *)
|
|
|
- log error "Partition ${part} of ${guestname} fstype is invalid '${fstype}'"
|
|
|
- return 1
|
|
|
- ;;
|
|
|
- esac
|
|
|
- else
|
|
|
- formatcmd=""
|
|
|
- fi
|
|
|
-
|
|
|
- # Attach disk to xen
|
|
|
- log verbose "Attaching partition ${part}"
|
|
|
- log_command "xl block-attach 0 \"phy:${devname}\" xvda w"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Attaching partition ${part} failed."
|
|
|
- return 1
|
|
|
- fi
|
|
|
-
|
|
|
-
|
|
|
- # Loop for 20s to wait until /dev/xvdaX appears
|
|
|
- i=0
|
|
|
- while [ ! -b /dev/xvda${part} ]; do
|
|
|
- ((i++))
|
|
|
- if [[ "$i" == '40' ]]; then
|
|
|
- break;
|
|
|
- fi
|
|
|
- sleep 0.5
|
|
|
- done
|
|
|
-
|
|
|
- if [ ! -b /dev/xvda${part} ]; then
|
|
|
- log error "Partition ${part} creation failed."
|
|
|
- return 2
|
|
|
- fi
|
|
|
-
|
|
|
- log verbose "/dev/xvda${part} created"
|
|
|
-
|
|
|
- if [ -n "${formatcmd}" ]; then
|
|
|
- log info "Creating filesystem for partition '${part}'"
|
|
|
- log_command "${formatcmd} /dev/xvda${part}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Creating filesystem for partition '${part}' failed."
|
|
|
- return 2
|
|
|
- fi
|
|
|
- fi
|
|
|
-
|
|
|
- case ${content} in
|
|
|
- *.img*)
|
|
|
- decompress=""
|
|
|
- case ${content} in
|
|
|
- *.img.gz)
|
|
|
- decompress='zcat'
|
|
|
- ;;
|
|
|
- *.img.bz2)
|
|
|
- decompress='bzcat'
|
|
|
- ;;
|
|
|
- *.img)
|
|
|
- decompress='cat'
|
|
|
- ;;
|
|
|
- *)
|
|
|
- # invalid/unknown compression type
|
|
|
- log error "Invalid file format in disk ${content}"
|
|
|
- return 2
|
|
|
- ;;
|
|
|
- esac
|
|
|
- # dd into partition
|
|
|
- log verbose "Populating partition '${part}'"
|
|
|
- log_command "xenguest-mkimage extract-disk-file ${guestfile} ${content} | ${decompress} | dd of=/dev/xvda${part} "
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Populating partition '${part}' failed."
|
|
|
- return 2
|
|
|
- fi
|
|
|
- ;;
|
|
|
- *.tar*)
|
|
|
- tararg=""
|
|
|
- case ${content} in
|
|
|
- *.tar.gz)
|
|
|
- tararg="z"
|
|
|
- ;;
|
|
|
- *.tar.bz2)
|
|
|
- tararg="j"
|
|
|
- ;;
|
|
|
- *.tar.xz)
|
|
|
- tararg="J"
|
|
|
- ;;
|
|
|
- *.tar)
|
|
|
- tararg=""
|
|
|
- ;;
|
|
|
- *)
|
|
|
- # invalid/unknown tar type
|
|
|
- log error "Invalid file format in disk ${content}"
|
|
|
- return 2
|
|
|
- ;;
|
|
|
- esac
|
|
|
-
|
|
|
- # must mount the partition and extract
|
|
|
- mntdir=$(mktemp -d)
|
|
|
- log verbose "Mounting partition '${part}'"
|
|
|
- log_command "mount /dev/xvda${part} ${mntdir}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Mounting partition '${part}' failed."
|
|
|
- rm -rf ${mntdir}
|
|
|
- return 2
|
|
|
- fi
|
|
|
-
|
|
|
- # tar and unmount
|
|
|
- log_command "xenguest-mkimage extract-disk-file ${guestfile} ${content} |" \
|
|
|
- "tar -C ${mntdir} -x${tararg}f - "
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Cannot populate partition ${part}"
|
|
|
- umount ${mntdir}
|
|
|
- rm -rf ${mntdir}
|
|
|
- return 2
|
|
|
- fi
|
|
|
- log_command "umount ${mntdir}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Unmounting ${part} failed."
|
|
|
- rm -rf ${mntdir}
|
|
|
- return 2
|
|
|
- fi
|
|
|
- rm -rf ${mntdir}
|
|
|
- ;;
|
|
|
- *)
|
|
|
- #invalid content type
|
|
|
- ;;
|
|
|
- esac
|
|
|
-
|
|
|
- # Detach disk
|
|
|
- xenguest_detach_disk
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- return 1
|
|
|
- fi
|
|
|
- fi
|
|
|
- done
|
|
|
-
|
|
|
- log info "Initialised disk for guest '${guestname}' successfully"
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_guest_create()
|
|
|
-{
|
|
|
- # extract xenguest tar
|
|
|
- # put xen config in etc ?
|
|
|
- # if disk config file:
|
|
|
- # disk init
|
|
|
- # add partititions
|
|
|
-
|
|
|
- guestfile="$1"
|
|
|
- guestname="$2"
|
|
|
-
|
|
|
- log info "Attempting to create guest '${guestname}' using ${guestfile}"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- log verbose "Cleaning up old directory"
|
|
|
- log_command verbose "rm -rf ${XENGUEST_CONF_BASE}/guests/${guestname}"
|
|
|
- log verbose "Creating directory for guest '${guestname}'"
|
|
|
- log_command "mkdir -p ${XENGUEST_CONF_BASE}/guests/${guestname}"
|
|
|
-
|
|
|
- log verbose "Extracting guest image"
|
|
|
- log_command "xenguest-mkimage extract-config ${guestfile} ${XENGUEST_CONF_BASE}/guests/${guestname}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log fatal "Extracting guest image failed."
|
|
|
- fi
|
|
|
-
|
|
|
- # Set guest name inside config
|
|
|
- log verbose "Setting guest name"
|
|
|
- log_command "xenguest-mkimage update ${XENGUEST_CONF_BASE}/guests/${guestname} --xen-name=${guestname}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log error "Setting guest name failed."
|
|
|
- xenguest_guest_remove ${guestname}
|
|
|
- exit 1
|
|
|
- fi
|
|
|
-
|
|
|
- xenguest_disk_init ${guestname} ${guestfile}
|
|
|
- disk_init_status=$?
|
|
|
- if [ $disk_init_status -ne 0 ]; then
|
|
|
- log error "Disk creation for guest '${guestname}' failed."
|
|
|
- if [ $disk_init_status -eq 2 ]; then
|
|
|
- xenguest_detach_disk
|
|
|
- fi
|
|
|
- xenguest_guest_remove ${guestname}
|
|
|
- exit 1
|
|
|
- fi
|
|
|
-
|
|
|
- log info "Guest '${guestname}' created successfully"
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_guest_remove()
|
|
|
-{
|
|
|
- guestname="$1"
|
|
|
- log info "Attempting to remove guest '${guestname}'"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- source ${XENGUEST_CONF_BASE}/guests/${guestname}/disk.cfg
|
|
|
- if [ -z "${DISK_DEVICE}" ]; then
|
|
|
- # By default guest is using disk defined inside xenguest-manager.conf
|
|
|
- diskdevice="${XENGUEST_VOLUME_DEVICE}"
|
|
|
- volumename="${XENGUEST_VOLUME_NAME}"
|
|
|
- else
|
|
|
- # If guest configuration contains custom disk setting,
|
|
|
- # overwrite default one
|
|
|
- diskdevice="${DISK_DEVICE}"
|
|
|
- volumename="vg-xen-$(basename ${diskdevice})"
|
|
|
- fi
|
|
|
- devname="/dev/${volumename}/${guestname}"
|
|
|
-
|
|
|
- # find and remove guest volume
|
|
|
- xenguest_volume_remove ${volumename} ${guestname}
|
|
|
- status=$?
|
|
|
-
|
|
|
- # remove guest files
|
|
|
- log info "Removing configuration files for guest '${guestname}'."
|
|
|
- log_command "rm -rf ${XENGUEST_CONF_BASE}/guests/${guestname}"
|
|
|
-
|
|
|
- if [ ${status} -ne 0 ]; then
|
|
|
- # Shouldn't log success message if volume removal fails
|
|
|
- exit 1
|
|
|
- fi
|
|
|
-
|
|
|
- log info "Removed guest '${guestname}' successfully"
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_call_inits()
|
|
|
-{
|
|
|
- # Inputs:
|
|
|
- # $1 - script directory
|
|
|
-
|
|
|
- local scriptdir
|
|
|
- local guestdir
|
|
|
- local guestcfgfile
|
|
|
- local guestname
|
|
|
-
|
|
|
- scriptdir="${1}"
|
|
|
- guestdir="${2}"
|
|
|
- guestcfgfile="${3}"
|
|
|
- guestname="${4}"
|
|
|
-
|
|
|
- log "Attempting to call all init scripts in ${scriptdir}"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
-
|
|
|
- init_scripts="$(find ${XENGUEST_CONF_BASE}/${scriptdir} -type f 2> /dev/null | \
|
|
|
- sort) $(find ${guestdir}/${scriptdir} -type f 2> /dev/null | sort)"
|
|
|
- for f in ${init_scripts}; do
|
|
|
- if [ -x "$f" ]; then
|
|
|
- log_source $f
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- rm -f ${guestcfgfile}
|
|
|
- popd > /dev/null 2>&1
|
|
|
- log fatal "Error during init script $(basename $f) of ${guestname}"
|
|
|
- fi
|
|
|
- else
|
|
|
- log fatal "$f is not executable. Exiting..."
|
|
|
- fi
|
|
|
- done
|
|
|
-
|
|
|
- if [ "${init_scripts}" = " " ]; then
|
|
|
- log "No scripts found"
|
|
|
- else
|
|
|
- log "All init scripts in ${scriptdir} completed successfully"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_guest_start()
|
|
|
-{
|
|
|
- guestname="${1}"
|
|
|
- guestdir=${XENGUEST_CONF_BASE}/guests/${guestname}
|
|
|
-
|
|
|
- log info "Attempting to start guest '${guestname}'"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- guestcfgfile=$(mktemp -u "${guestname}.XXXXXX" --tmpdir="${guestdir}" --suffix=".cfg")
|
|
|
-
|
|
|
- # Get guest configuration
|
|
|
- source ${guestdir}/params.cfg
|
|
|
-
|
|
|
- pushd ${guestdir} > /dev/null 2>&1
|
|
|
-
|
|
|
- # create config by merging all configurations together
|
|
|
- cat guest.cfg $(find guest.d -type f 2> /dev/null) > ${guestcfgfile}
|
|
|
-
|
|
|
- # Build init script lists (ignore non existing dirs errors,
|
|
|
- # sort alphabetically and run global scripts first)
|
|
|
- #
|
|
|
- # These scripts are sourced throughout the start operation if they
|
|
|
- # are executable
|
|
|
- init_pre="init.pre"
|
|
|
- init_d="init.d"
|
|
|
- init_post="init.post"
|
|
|
-
|
|
|
- # call pre init scripts
|
|
|
- xenguest_call_inits "${init_pre}" "${guestdir}" "${guestcfgfile}" "${guestname}"
|
|
|
-
|
|
|
- # Create non started guest
|
|
|
- log verbose "Initiating ${guestname}"
|
|
|
- log_command "xl create -p ${guestcfgfile}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- rm -f ${guestcfgfile}
|
|
|
- popd > /dev/null 2>&1
|
|
|
- log fatal "Initiating ${guestname} failed."
|
|
|
- fi
|
|
|
-
|
|
|
- # call init scripts
|
|
|
- xenguest_call_inits "${init_d}" "${guestdir}" "${guestcfgfile}" "${guestname}"
|
|
|
-
|
|
|
- # Start guest
|
|
|
- log info "Starting ${guestname}"
|
|
|
- log_command "xl unpause ${guestname}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- rm -f ${guestcfgfile}
|
|
|
- popd > /dev/null 2>&1
|
|
|
- log fatal "Starting ${guestname} failed."
|
|
|
- fi
|
|
|
-
|
|
|
- # call post init scripts
|
|
|
- xenguest_call_inits "${init_post}" "${guestdir}" "${guestcfgfile}" "${guestname}"
|
|
|
-
|
|
|
- rm -f ${guestcfgfile}
|
|
|
- popd > /dev/null 2>&1
|
|
|
-
|
|
|
- log info "Guest '${guestname}' started successfully"
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_guest_stop()
|
|
|
-{
|
|
|
- local guestname
|
|
|
- local extra_arg
|
|
|
-
|
|
|
- guestname="${1}"
|
|
|
- extra_arg="${2}"
|
|
|
-
|
|
|
- shutdown_args=""
|
|
|
-
|
|
|
- log info "Attempting to stop guest '${guestname}'"
|
|
|
-
|
|
|
- if [[ ${extra_arg} != "--nowait" ]]; then
|
|
|
- shutdown_args+=" -w"
|
|
|
- fi
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- log_command "xl shutdown ${shutdown_args} ${guestname}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- if [[ ${extra_arg} == "--kill" ]]; then
|
|
|
- log info "Stopping '${guestname}' failed, calling kill..."
|
|
|
- xenguest_guest_kill "${guestname}"
|
|
|
- else
|
|
|
- log fatal "Stopping guest '${guestname}' failed."
|
|
|
- fi
|
|
|
- fi
|
|
|
- if [[ "${extra_arg}" != "--nowait" ]]; then
|
|
|
- log info "Guest '${guestname}' stopped successfully"
|
|
|
- else
|
|
|
- log info "xl shutdown exited successfully for guest '${guestname}'."
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function xenguest_guest_kill()
|
|
|
-{
|
|
|
- local guestname
|
|
|
-
|
|
|
- guestname="${1}"
|
|
|
- log "Attempting to kill guest '${guestname}'"
|
|
|
-
|
|
|
- check_private
|
|
|
-
|
|
|
- log_command "xl destroy ${guestname}"
|
|
|
- if [ $? -ne 0 ]; then
|
|
|
- log "fatal:Killing guest '${guestname}' failed."
|
|
|
- fi
|
|
|
- log "Guest '${guestname}' killed successfully"
|
|
|
-}
|
|
|
-
|
|
|
-# Private
|
|
|
-function check_guest_arg()
|
|
|
-{
|
|
|
- check_private
|
|
|
-
|
|
|
- cmd="${1}"
|
|
|
- guestname="${2:-}"
|
|
|
- if [ -z "${guestname:-}" ]; then
|
|
|
- log fatal "Usage ${this} ${cmd} GUESTNAME"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-function check_guest_exist()
|
|
|
-{
|
|
|
- guestname="${1}"
|
|
|
- if [ ! -f ${XENGUEST_CONF_BASE}/guests/${guestname}/guest.cfg -o \
|
|
|
- ! -f ${XENGUEST_CONF_BASE}/guests/${guestname}/params.cfg ]; then
|
|
|
- log fatal "Invalid guest name '${guestname}'"
|
|
|
- fi
|
|
|
-
|
|
|
- log verbose "Guest '${guestname}' found: ${XENGUEST_CONF_BASE}/guests/${guestname}/"
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-function xenguest_list_guests()
|
|
|
-{
|
|
|
- guestlist=""
|
|
|
- if [ -d ${XENGUEST_CONF_BASE}/guests ]; then
|
|
|
- guestlist=$(find ${XENGUEST_CONF_BASE}/guests -mindepth 1 -maxdepth 1 -type d -exec sh -c 'if [ -f {}/guest.cfg ]; then basename {}; fi' \;)
|
|
|
- else
|
|
|
- log "Info: Guests directory ${XENGUEST_CONF_BASE}/guests not found"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-function xl_list_contains()
|
|
|
-{
|
|
|
- guestname="${1}"
|
|
|
- # Select first column of xl list, and find guestname exactly using regex
|
|
|
- running=$(xl list | awk 'NR > 1 {print $1}' | grep "^${guestname}$" || echo)
|
|
|
- if [ "${running}" = "${guestname}" ]; then
|
|
|
- log verbose "Guest '${guestname}' is running"
|
|
|
- return 0
|
|
|
- fi
|
|
|
-
|
|
|
- log verbose "Guest '${guestname}' is not running"
|
|
|
-
|
|
|
- return 1
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-function check_guest_running()
|
|
|
-{
|
|
|
- guestname="${1}"
|
|
|
- if ! xl_list_contains $guestname; then
|
|
|
- log fatal "Cannot ${cmd} guest '${guestname}', already stopped"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-# Public
|
|
|
-function check_guest_not_running()
|
|
|
-{
|
|
|
- guestname="${1}"
|
|
|
- if xl_list_contains $guestname; then
|
|
|
- log fatal "Cannot ${cmd} guest '${guestname}', already started"
|
|
|
- fi
|
|
|
-}
|
|
|
-
|
|
|
-## Entry Point ##
|
|
|
-
|
|
|
-# Check for verbose level arguments, and shift if found
|
|
|
-case ${1:-help} in
|
|
|
- -v|-V)
|
|
|
- LOG_LEVEL="INFO"
|
|
|
- shift
|
|
|
- ;;
|
|
|
- -vv|-VV)
|
|
|
- LOG_LEVEL="VERBOSE"
|
|
|
- shift
|
|
|
- ;;
|
|
|
-esac
|
|
|
-
|
|
|
-# Limit Verbose list to only those desired to be shown
|
|
|
-LOG_LEVEL_LIST=${LOG_LEVEL_LIST//${LOG_LEVEL}*/${LOG_LEVEL}}
|
|
|
-
|
|
|
-log ""
|
|
|
-log "Arguments: $*"
|
|
|
-
|
|
|
-cmd="${1:-help}"
|
|
|
-arg1="${2:-}"
|
|
|
-arg2="${3:-}"
|
|
|
-
|
|
|
-case ${cmd} in
|
|
|
- help|--help|-h|-?)
|
|
|
- usage
|
|
|
- exit 0
|
|
|
- ;;
|
|
|
-esac
|
|
|
-
|
|
|
-# Check if we have a valid Dom0 booted with Xen
|
|
|
-log_command "xl info"
|
|
|
-if [ $? -ne 0 ]; then
|
|
|
- log error "Xen environment is not valid!!!"
|
|
|
- log error "Check if Xen has booted and the kernel configuration."
|
|
|
- log fatal "More information in the logfile: ${LOGFILE}"
|
|
|
-fi
|
|
|
-
|
|
|
-case ${cmd} in
|
|
|
- check-xen)
|
|
|
- log verbose "Valid Xen environment found"
|
|
|
- exit 0
|
|
|
- ;;
|
|
|
- create)
|
|
|
- guestfile="${arg1}"
|
|
|
- guestname="${arg2}"
|
|
|
- # guestfile invalid if empty
|
|
|
- if [ -z "${guestfile}" ]; then
|
|
|
- log fatal "Usage ${this} create XENGUEST_FILE [NAME]"
|
|
|
- fi
|
|
|
- # Set Guest name before resolving any symlinks
|
|
|
- if [ -z "${guestname}" ]; then
|
|
|
- guestname=$(basename ${guestfile} .xenguest)
|
|
|
- log info "guestname argument not provided, using '${guestname}'"
|
|
|
- fi
|
|
|
- # Check for and resolve symlink
|
|
|
- if [ -L "${guestfile}" ]; then
|
|
|
- errmsg="'${guestfile}' is a broken symlink"
|
|
|
- guestfile=$(readlink -e "${guestfile}")
|
|
|
- if [ -z "${guestfile}" ]; then
|
|
|
- log fatal "${errmsg}"
|
|
|
- else
|
|
|
- log info "Guestfile symlink resolved to path '${guestfile}'"
|
|
|
- fi
|
|
|
- fi
|
|
|
- # Check that guestfile is a valid file
|
|
|
- if [ ! -f "${guestfile}" ]; then
|
|
|
- log fatal "File '${guestfile}' not found"
|
|
|
- fi
|
|
|
- # Check if guest already exists
|
|
|
- if [ -f ${XENGUEST_CONF_BASE}/guests/${guestname}/guest.cfg ]; then
|
|
|
- log fatal "Guest '${guestname}' already exists"
|
|
|
- fi
|
|
|
-
|
|
|
- xenguest_guest_create ${guestfile} ${guestname}
|
|
|
- ;;
|
|
|
- remove)
|
|
|
- guestname="${arg1:-}"
|
|
|
- check_guest_arg ${cmd} ${guestname}
|
|
|
- check_guest_exist ${guestname}
|
|
|
- # We need to stop the guest first if it is running
|
|
|
- if xl_list_contains $guestname; then
|
|
|
- xenguest_guest_kill ${guestname}
|
|
|
- fi
|
|
|
- xenguest_guest_remove ${guestname}
|
|
|
- ;;
|
|
|
- start)
|
|
|
- guestname="${arg1:-}"
|
|
|
- check_guest_arg ${cmd} ${guestname}
|
|
|
- check_guest_exist ${guestname}
|
|
|
- check_guest_not_running ${guestname}
|
|
|
- xenguest_guest_start ${guestname}
|
|
|
- ;;
|
|
|
- stop|shutdown)
|
|
|
- guestname="${arg1:-}"
|
|
|
- extra_arg="${arg2:-}"
|
|
|
- check_guest_arg ${cmd} ${guestname}
|
|
|
- check_guest_exist ${guestname}
|
|
|
- check_guest_running ${guestname}
|
|
|
- xenguest_guest_stop "${guestname}" "${extra_arg}"
|
|
|
- ;;
|
|
|
- kill|destroy)
|
|
|
- guestname="${arg1:-}"
|
|
|
- check_guest_arg ${cmd} ${guestname}
|
|
|
- check_guest_exist ${guestname}
|
|
|
- check_guest_running ${guestname}
|
|
|
- xenguest_guest_kill ${guestname}
|
|
|
- ;;
|
|
|
- list)
|
|
|
- xenguest_list_guests
|
|
|
- echo ${guestlist} | tr " " "\n"
|
|
|
- ;;
|
|
|
- status)
|
|
|
-
|
|
|
- single_status() {
|
|
|
- guestname="${1}"
|
|
|
- check_guest_exist ${guestname}
|
|
|
- if xl_list_contains $guestname; then
|
|
|
- echo "${guestname} Running"
|
|
|
- else
|
|
|
- echo "${guestname} Stopped"
|
|
|
- fi
|
|
|
- }
|
|
|
-
|
|
|
- guestname="${arg1}"
|
|
|
- if [ -n "${guestname}" ]; then
|
|
|
- single_status ${guestname}
|
|
|
- else
|
|
|
- xenguest_list_guests
|
|
|
- if [ -n "${guestlist}" ]; then
|
|
|
- for f in ${guestlist}; do
|
|
|
- single_status $f
|
|
|
- done
|
|
|
- fi
|
|
|
- fi
|
|
|
- ;;
|
|
|
- *)
|
|
|
- log fatal "Invalid argument: ${cmd}"
|
|
|
- ;;
|
|
|
-esac
|
|
|
-
|