2023-02-17 17:37:16 +01:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
#
|
|
|
|
|
# Purpose {{{
|
|
|
|
|
# This script will try to start an iDrac 6/7 virtual console
|
|
|
|
|
# 1. Ask for an host if not given with the option.
|
|
|
|
|
# 2. Ask for a user if not given with the option.
|
|
|
|
|
# 3. Ask for the password.
|
|
|
|
|
#
|
|
|
|
|
# Inspired from : https://gist.github.com/xbb/4fd651c2493ad9284dbcb827dc8886d6
|
|
|
|
|
#
|
|
|
|
|
# 2023-02-15
|
|
|
|
|
# }}}
|
|
|
|
|
# Flags {{{
|
|
|
|
|
## Exit on error {{{
|
|
|
|
|
set -o errexit
|
|
|
|
|
## }}}
|
|
|
|
|
## Exit on unset var {{{
|
|
|
|
|
### Use "${VARNAME-}" to test a var that may not have been set
|
|
|
|
|
set -o nounset
|
|
|
|
|
## }}}
|
|
|
|
|
## Pipeline command is treated as failed {{{
|
|
|
|
|
### Not available in POSIX sh − https://github.com/koalaman/shellcheck/wiki/SC3040
|
|
|
|
|
#set -o pipefail
|
|
|
|
|
## }}}
|
|
|
|
|
## Help with debugging {{{
|
|
|
|
|
### Call the script by prefixing it with "TRACE=1 ./script.sh"
|
|
|
|
|
if [ "${TRACE-0}" -eq 1 ]; then set -o xtrace; fi
|
|
|
|
|
## }}}
|
|
|
|
|
# }}}
|
|
|
|
|
# Vars {{{
|
|
|
|
|
PROGNAME=$(basename "${0}"); readonly PROGNAME
|
|
|
|
|
PROGDIR=$(readlink --canonicalize-missing $(dirname "${0}")); readonly PROGDIR
|
|
|
|
|
ARGS="${*}"; readonly ARGS
|
|
|
|
|
readonly NBARGS="${#}"
|
|
|
|
|
[ -z "${DEBUG-}" ] && DEBUG=1
|
|
|
|
|
## Export DEBUG for sub-script
|
|
|
|
|
export DEBUG
|
|
|
|
|
|
|
|
|
|
## Default values for some vars
|
|
|
|
|
readonly IDRAC_USERNAME_DEFAULT="root"
|
|
|
|
|
readonly IDRAC_PORT_DEFAULT="443"
|
|
|
|
|
readonly JAVA_DISABLE_ALGO_DEFAULT="SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, EC keySize < 224, anon, NULL, include jdk.disabled.namedCurves"
|
|
|
|
|
readonly JAVA_SECURITY_FILENAME="idrac.java.security"
|
|
|
|
|
readonly TEMP_DIR_BASE="/tmp"
|
|
|
|
|
|
|
|
|
|
## Colors
|
|
|
|
|
readonly PURPLE='\033[1;35m'
|
|
|
|
|
readonly RED='\033[0;31m'
|
|
|
|
|
readonly RESET='\033[0m'
|
|
|
|
|
readonly COLOR_DEBUG="${PURPLE}"
|
|
|
|
|
# }}}
|
|
|
|
|
usage() { # {{{
|
|
|
|
|
|
|
|
|
|
cat <<- HELP
|
|
|
|
|
usage: $PROGNAME [-d|-h] [--user idrac_username] [--host idrac.hostname] [--port port.number] [--temp /path/to/temp/directory]
|
|
|
|
|
|
|
|
|
|
Try to start an iDrac 6/7 virtual console with given informations.
|
|
|
|
|
|
|
|
|
|
The password will always be asked and not displayed.
|
|
|
|
|
|
|
|
|
|
EXAMPLES :
|
|
|
|
|
- Start the script and wait for prompt to enter informations
|
|
|
|
|
${PROGNAME}
|
|
|
|
|
|
|
|
|
|
- Start virtual console of a given host
|
|
|
|
|
${PROGNAME} --host "oob-myserver.domain.tld"
|
|
|
|
|
|
|
|
|
|
- Start virtual console with SSH Port forwarding
|
|
|
|
|
${PROGNAME} --host "localhost:7200"
|
|
|
|
|
|
|
|
|
|
- Use specific username
|
|
|
|
|
${PROGNAME} --user "admin"
|
|
|
|
|
|
|
|
|
|
- Store downloaded files and informations in a specific temp directory
|
|
|
|
|
${PROGNAME} --temp /tmp/my.idrac.temp.dir
|
|
|
|
|
|
|
|
|
|
OPTIONS :
|
|
|
|
|
-d,--debug
|
|
|
|
|
Enable debug messages.
|
|
|
|
|
|
|
|
|
|
-h,--help
|
|
|
|
|
Print this help message.
|
|
|
|
|
|
|
|
|
|
--host
|
|
|
|
|
Define the iDrac host to connect.
|
|
|
|
|
Fallback: It will be asked to the user.
|
|
|
|
|
|
|
|
|
|
-p,--port
|
|
|
|
|
Define the iDrac port to use.
|
|
|
|
|
The script will try to parse the given value of hostname and extract a port number
|
|
|
|
|
Fallback: It will be asked to the user.
|
|
|
|
|
Default : ${IDRAC_PORT_DEFAULT}
|
|
|
|
|
|
|
|
|
|
-u,--user
|
|
|
|
|
Define the iDrac username.
|
|
|
|
|
Fallback: It will be asked to the user.
|
|
|
|
|
Default : ${IDRAC_USERNAME_DEFAULT}
|
|
|
|
|
|
|
|
|
|
--temp,--temp-dir,--temp-directory
|
|
|
|
|
Define the temp directoy to store downloaded files and informations.
|
|
|
|
|
Default : ${TEMP_DIR_BASE}/idrac.given_hostname
|
|
|
|
|
|
|
|
|
|
--java-disable-algo
|
|
|
|
|
Override global java security settings.
|
|
|
|
|
To allow old console to work on new java environment.
|
|
|
|
|
Default : ${JAVA_DISABLE_ALGO_DEFAULT}
|
|
|
|
|
For more informations, see : https://gist.github.com/xbb/4fd651c2493ad9284dbcb827dc8886d6?permalink_comment_id=4006481#gistcomment-4006481
|
|
|
|
|
HELP
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
debug_message() { # {{{
|
|
|
|
|
|
|
|
|
|
local_debug_message="${1}"
|
|
|
|
|
|
|
|
|
|
## Print message if DEBUG is enable (=0)
|
|
|
|
|
[ "${DEBUG}" -eq "0" ] && printf '\e[1;35m%-6b\e[m\n' "DEBUG − ${PROGNAME} : ${local_debug_message}"
|
|
|
|
|
|
|
|
|
|
unset local_debug_message
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
error_message() { # {{{
|
|
|
|
|
|
|
|
|
|
local_error_message="${1}"
|
|
|
|
|
local_error_code="${2}"
|
|
|
|
|
|
|
|
|
|
## Print message
|
|
|
|
|
printf '%b\n' "ERROR − ${PROGNAME} : ${RED}${local_error_message}${RESET}" >&2
|
|
|
|
|
|
|
|
|
|
unset local_error_message
|
|
|
|
|
|
|
|
|
|
exit "${local_error_code:=66}"
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
define_vars() { # {{{
|
|
|
|
|
|
|
|
|
|
## If idrac_host_temp wasn't defined (argument) {{{
|
|
|
|
|
if [ -z "${idrac_host_temp-}" ]; then
|
|
|
|
|
### Ask the user to enter a username
|
|
|
|
|
printf -- '%b' 'Enter iDrac host (eg. "oob-myserver.domain.tld", "localhost:7200"…): '
|
|
|
|
|
read -r idrac_host_temp
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
## If idrac_host_temp is still empty {{{
|
|
|
|
|
if [ -z "${idrac_host_temp-}" ]; then
|
|
|
|
|
error_message "iDrac host can't be empty, please check your input or use --host option. For more informations, see the help message (--help)." 11
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
## If idrac_host_temp contains a port number {{{
|
2023-02-27 11:40:35 +01:00
|
|
|
|
if printf '%b' "${idrac_host_temp}" | grep --extended-regexp '^[a-zA-Z]+.*:[0-9]+$' >/dev/null; then
|
|
|
|
|
debug_message "- Try to extract hostname and port number from value (${RED}${idrac_host_temp}${COLOR_DEBUG})."
|
2023-02-17 17:37:16 +01:00
|
|
|
|
### Extract hostname and port number
|
|
|
|
|
idrac_host=$(printf '%b' "${idrac_host_temp}" | awk 'BEGIN { FS = ":" } ; /^[a-zA-Z]+.*:[0-9]+$/ { print $1 }')
|
|
|
|
|
idrac_port=$(printf '%b' "${idrac_host_temp}" | awk 'BEGIN { FS = ":" } ; /^[a-zA-Z]+.*:[0-9]+$/ { print $2 }')
|
|
|
|
|
else
|
|
|
|
|
### Simply use idrac_host_temp as hostname
|
|
|
|
|
idrac_host="${idrac_host_temp}"
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
|
|
|
|
|
## If idrac_username wasn't defined (argument) {{{
|
|
|
|
|
if [ -z "${idrac_username-}" ]; then
|
|
|
|
|
### Ask the user to enter a username
|
|
|
|
|
printf -- '%b' "Enter iDrac username (default ${RED}${IDRAC_USERNAME_DEFAULT}${RESET}): "
|
|
|
|
|
read -r idrac_username
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
## If idrac_username is still empty {{{
|
|
|
|
|
if [ -z "${idrac_username-}" ]; then
|
|
|
|
|
### Use default value
|
|
|
|
|
readonly idrac_username="${IDRAC_USERNAME_DEFAULT}"
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
|
|
|
|
|
## If idrac_port wasn't defined (argument or by idrac_host_temp) {{{
|
|
|
|
|
if [ -z "${idrac_port-}" ]; then
|
|
|
|
|
### Ask the user to enter a port number
|
|
|
|
|
printf -- '%b' "Enter iDrac port (default ${RED}${IDRAC_PORT_DEFAULT}${RESET}): "
|
|
|
|
|
read -r idrac_port
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
## If idrac_port is still empty {{{
|
|
|
|
|
if [ -z "${idrac_port-}" ]; then
|
|
|
|
|
idrac_port="${IDRAC_PORT_DEFAULT}"
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
|
|
|
|
|
## Ask for idrac_password {{{
|
|
|
|
|
printf -- '%b' "Enter iDrac password (it won't be displayed): "
|
|
|
|
|
read -r -s idrac_password
|
|
|
|
|
printf -- '%b\n' ""
|
|
|
|
|
## }}}
|
|
|
|
|
## If idrac_password is empty, ask again {{{
|
|
|
|
|
if [ -z "${idrac_password-}" ]; then
|
|
|
|
|
printf -- '%b' "A password is required to connect iDrac host (it won't be displayed): "
|
|
|
|
|
read -r -s idrac_password
|
|
|
|
|
printf -- '%b\n' ""
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
## If idrac_password is still empty {{{
|
|
|
|
|
if [ -z "${idrac_password-}" ]; then
|
|
|
|
|
error_message "iDrac password can't be empty, please check your input. For more informations, see the help message (--help)." 12
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
|
|
|
|
|
## If java_disable_algo wasn't defined (argument) {{{
|
|
|
|
|
if [ -z "${java_disable_algo-}" ]; then
|
|
|
|
|
### Use default value
|
|
|
|
|
readonly java_disable_algo="${JAVA_DISABLE_ALGO_DEFAULT}"
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
## If temp_dir wasn't defined (argument) {{{
|
|
|
|
|
if [ -z "${temp_dir-}" ]; then
|
|
|
|
|
### Use default value
|
|
|
|
|
readonly temp_dir="${TEMP_DIR_BASE}/idrac.${idrac_host}"
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
|
|
|
|
|
is_command_available() { # {{{
|
|
|
|
|
|
|
|
|
|
local_command_available_cmd="${1}"
|
|
|
|
|
debug_prefix="${2:-}"
|
|
|
|
|
|
|
|
|
|
## Return False by default
|
|
|
|
|
return_command_available="1"
|
|
|
|
|
|
|
|
|
|
if [ "$(command -v ${local_command_available_cmd})" ]; then
|
|
|
|
|
debug_message "${debug_prefix}is_command_available − \
|
|
|
|
|
${RED}${local_command_available_cmd}${COLOR_DEBUG} seems present on this host."
|
|
|
|
|
return_command_available="0"
|
|
|
|
|
else
|
|
|
|
|
debug_message "${debug_prefix}is_command_available − \
|
|
|
|
|
${RED}${local_command_available_cmd}${COLOR_DEBUG} is not available on this host."
|
|
|
|
|
return_command_available="1"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
unset local_command_available_cmd
|
|
|
|
|
unset debug_prefix
|
|
|
|
|
|
|
|
|
|
return "${return_command_available}"
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
is_file_empty() { # {{{
|
|
|
|
|
|
|
|
|
|
local_file="${1}"
|
|
|
|
|
debug_prefix="${2:-}"
|
|
|
|
|
|
|
|
|
|
## File is empty by default
|
|
|
|
|
return_is_file_empty="0"
|
|
|
|
|
|
|
|
|
|
## Check if the file is empty
|
|
|
|
|
if [ ! -s "${local_file}" ]; then
|
|
|
|
|
return_is_file_empty="0"
|
|
|
|
|
debug_message "${debug_prefix}is_file_empty − \
|
|
|
|
|
The file ${RED}${local_file}${COLOR_DEBUG} is empty or doesn't exists."
|
|
|
|
|
else
|
|
|
|
|
return_is_file_empty="1"
|
|
|
|
|
debug_message "${debug_prefix}is_file_empty − \
|
|
|
|
|
The file ${RED}${local_file}${COLOR_DEBUG} exists and has a size greater than zero."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
unset local_file
|
|
|
|
|
unset debug_prefix
|
|
|
|
|
|
|
|
|
|
return "${return_is_file_empty}"
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
|
|
|
|
|
main() { # {{{
|
|
|
|
|
|
|
|
|
|
debug_message "--- MAIN BEGIN"
|
|
|
|
|
|
|
|
|
|
## If java command is not available {{{
|
|
|
|
|
### Exit with error message
|
|
|
|
|
is_command_available "java" "| " \
|
|
|
|
|
|| error_message "No java command available. Please install a openjdk-*-jre package with your package manager." 01
|
|
|
|
|
## }}}
|
|
|
|
|
## If wget command is not available {{{
|
|
|
|
|
### Exit with error message
|
|
|
|
|
is_command_available "wget" "| " \
|
|
|
|
|
|| error_message "No wget command available. Please install =>wget<= package with your package manager." 02
|
|
|
|
|
## }}}
|
|
|
|
|
|
|
|
|
|
## Define all vars
|
2023-02-27 11:40:35 +01:00
|
|
|
|
debug_message "| Define vars BEGIN"
|
2023-02-17 17:37:16 +01:00
|
|
|
|
define_vars
|
2023-02-27 11:40:35 +01:00
|
|
|
|
debug_message "| Define vars END"
|
2023-02-17 17:37:16 +01:00
|
|
|
|
|
|
|
|
|
debug_message "| Given informations are :
|
|
|
|
|
idrac_host: ${RED}${idrac_host}${COLOR_DEBUG}
|
|
|
|
|
idrac_port: ${RED}${idrac_port}${COLOR_DEBUG}
|
|
|
|
|
idrac_username: ${RED}${idrac_username}${COLOR_DEBUG}
|
|
|
|
|
idrac_password: ${RED}HIDDEN${COLOR_DEBUG}
|
|
|
|
|
temp_dir: ${RED}${temp_dir}${COLOR_DEBUG}"
|
|
|
|
|
|
|
|
|
|
## Create and move to temp directory
|
|
|
|
|
debug_message "| Create and move to temp directory (${RED}${temp_dir}${COLOR_DEBUG})."
|
|
|
|
|
mkdir --parents -- "${temp_dir}"
|
|
|
|
|
cd -- "${temp_dir}" || error_message "Can't move to temp directory (${temp_dir})." 21
|
|
|
|
|
|
|
|
|
|
## Set Java security parameters
|
|
|
|
|
debug_message "| Set java security parameters (to ${RED}${JAVA_SECURITY_FILENAME}${COLOR_DEBUG} file)."
|
|
|
|
|
true > "${JAVA_SECURITY_FILENAME}"
|
|
|
|
|
printf '%b\n' "jdk.tls.disabledAlgorithms=${JAVA_DISABLE_ALGO_DEFAULT}" >> "${JAVA_SECURITY_FILENAME}"
|
|
|
|
|
|
|
|
|
|
## Try to download main jar file from idrac host
|
2023-05-04 09:38:12 +02:00
|
|
|
|
avctkvm_filename="avctKVM.jar"
|
|
|
|
|
avctkvm_url="https://${idrac_host}:${idrac_port}/software/${avctkvm_filename}"
|
|
|
|
|
debug_message "| Try to download ${RED}${avctkvm_filename}${COLOR_DEBUG} file from URL (${RED}${avctkvm_url}${COLOR_DEBUG})."
|
2023-02-17 17:37:16 +01:00
|
|
|
|
wget --timestamping --no-check-certificate --quiet -- "${avctkvm_url}"
|
2023-05-04 09:38:12 +02:00
|
|
|
|
is_file_empty "${avctkvm_filename}" "| " \
|
|
|
|
|
&& error_message "Downloaded ${avctkvm_filename} file is empty (from ${avctkvm_url} URL)." 22
|
2023-02-17 17:37:16 +01:00
|
|
|
|
|
|
|
|
|
## Librairies
|
|
|
|
|
## TODO? Download avctKVMIOLinux64.jar and avctVMLinux64.jar files to extract librairies (for keyboard…).
|
|
|
|
|
|
|
|
|
|
## Try to start iDrac console with java
|
|
|
|
|
printf '%b\n' "Try to start ${RED}${idrac_host}:${idrac_port}${RESET} virtual console with user (${RED}${idrac_username}${RESET}) and given password."
|
2023-05-04 09:38:12 +02:00
|
|
|
|
java -cp "${avctkvm_filename}" -Djava.security.properties=idrac.java.security -Djava.library.path=./lib com.avocent.idrac.kvm.Main ip="${idrac_host}" kmport=5900 vport=5900 user="${idrac_username}" passwd="${idrac_password}" apcp=1 version=2 vmprivilege=true "helpurl=https://${idrac_host}:${idrac_port}/help/contents.html"
|
2023-02-17 17:37:16 +01:00
|
|
|
|
|
|
|
|
|
cd -- - >/dev/null || error_message "Can't move to previous directory (before ${temp_dir})." 23
|
|
|
|
|
|
|
|
|
|
debug_message "--- MAIN END"
|
|
|
|
|
}
|
|
|
|
|
# }}}
|
|
|
|
|
|
|
|
|
|
# Manage arguments # {{{
|
|
|
|
|
# This code can't be in a function due to argument management
|
|
|
|
|
|
|
|
|
|
if [ ! "${NBARGS}" -eq "0" ]; then
|
|
|
|
|
|
|
|
|
|
manage_arg="0"
|
|
|
|
|
|
|
|
|
|
## If the first argument ask for help (h|help|-h|-help|-*h|-*help) {{{
|
|
|
|
|
if printf -- '%s' "${1-}" | grep --quiet --extended-regexp -- "^-*h(elp)?$"; then
|
|
|
|
|
usage
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
## }}}
|
|
|
|
|
|
|
|
|
|
## If the first argument is not an option
|
|
|
|
|
if ! printf -- '%s' "${1}" | grep --quiet --extended-regexp -- "^-+";
|
|
|
|
|
then
|
|
|
|
|
## Print help message and exit
|
|
|
|
|
printf '%b\n' "${RED}Invalid option: ${1}${RESET}"
|
|
|
|
|
printf '%b\n' "---"
|
|
|
|
|
usage
|
|
|
|
|
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Parse all options (start with a "-") one by one
|
|
|
|
|
while printf -- '%s' "${1-}" | grep --quiet --extended-regexp -- "^-+"; do
|
|
|
|
|
|
|
|
|
|
case "${1}" in
|
|
|
|
|
-d|--debug ) ## debug
|
|
|
|
|
DEBUG=0
|
|
|
|
|
debug_message "--- Manage argument BEGIN"
|
|
|
|
|
;;
|
|
|
|
|
--host ) ## Define idrac_host_temp with given arg
|
|
|
|
|
## Move to the next argument
|
|
|
|
|
shift
|
|
|
|
|
## Define var
|
|
|
|
|
readonly idrac_host_temp="${1}"
|
|
|
|
|
;;
|
|
|
|
|
-p,--port ) ## Define idrac_port with given arg
|
|
|
|
|
## Move to the next argument
|
|
|
|
|
shift
|
|
|
|
|
## Define var
|
|
|
|
|
readonly idrac_port="${1}"
|
|
|
|
|
;;
|
|
|
|
|
-u|--user ) ## Define idrac_username with given arg
|
|
|
|
|
## Move to the next argument
|
|
|
|
|
shift
|
|
|
|
|
## Define var
|
|
|
|
|
readonly idrac_username="${1}"
|
|
|
|
|
;;
|
|
|
|
|
--temp,--temp-dir,--temp-directory ) ## Define temp_dir with given arg
|
|
|
|
|
## Move to the next argument
|
|
|
|
|
shift
|
|
|
|
|
## Define var
|
|
|
|
|
readonly temp_dir="${1}"
|
|
|
|
|
;;
|
|
|
|
|
--java-disable-algo ) ## Define java_disable_algo with given arg
|
|
|
|
|
## Move to the next argument
|
|
|
|
|
shift
|
|
|
|
|
## Define var
|
|
|
|
|
readonly java_disable_algo="${1}"
|
|
|
|
|
;;
|
|
|
|
|
* ) ## unknow option
|
|
|
|
|
printf '%b\n' "${RED}Invalid option: ${1}${RESET}"
|
|
|
|
|
printf '%b\n' "---"
|
|
|
|
|
usage
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
debug_message "| ${RED}${1}${COLOR_DEBUG} option managed."
|
|
|
|
|
|
|
|
|
|
## Move to the next argument
|
|
|
|
|
shift
|
|
|
|
|
manage_arg=$((manage_arg+1))
|
|
|
|
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
debug_message "| ${RED}${manage_arg}${COLOR_DEBUG} argument(s) successfully managed."
|
|
|
|
|
else
|
|
|
|
|
debug_message "| No arguments/options to manage."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
debug_message "--- Manage argument END"
|
|
|
|
|
# }}}
|
|
|
|
|
|
|
|
|
|
main
|
|
|
|
|
|
|
|
|
|
exit 0
|