#!/bin/bash
if [ `id -u` -ne 0 ];then
echo "only a user with the root permission can run this script."
exit 1
fi
declare -r SCRIPT_PATH=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
declare -r SCRIPT_NAME=$0
echo "SCRIPT_PATH : ${SCRIPT_PATH}"
declare PACKAGE_PATH=`dirname ${SCRIPT_PATH}`
declare USER_NAME=""
declare HOST_IPS=""
declare HOST_IPS_ARR=""
declare HOST_IPS_ARRAY=""
declare HOST_NAMES=""
declare HOST_NAMES_ARRAY=
declare USER_GROUP="dbgrp"
declare PORT="20050"
declare XML_DIR=${SCRIPT_PATH}/one_master_one_slave_template.xml
declare INSTALL_PATH=""
declare SYSTEM_ARCH=""
declare SYSTEM_NAME=""
declare PASSWORD=""
function print_help()
{
echo "Usage: $0 [OPTION]
-?|--help show help information
-U|--user_name cluster user
-H|--host_ip intranet ip address of the host in the backend storage network(host1,host2)
-G|--user_grp group of the cluster user(default value dbgrp)
-p|--port database server port(default value 20050)
-D|--install_location installation directory of the openGauss program(default value ~/cluser)
-X|--xml_location cluster xml configuration file path
"
}
function die()
{
echo -e "\033[31merror:\033[0m $1"
exit 1
}
function warn()
{
echo -e "\033[33mwarnning:\033[0m $1"
sleep 2s
}
function info()
{
echo -e "\033[32minfo:\033[0m $1"
}
build_safe_command() {
local cmd=("$@")
local result=""
local first=true
for arg in "${cmd[@]}"; do
if $first; then
first=false
else
result+=" "
fi
result+=$(printf "%q" "$arg")
done
echo "$result"
}
function expect_ssh()
{
export EXPECT_PASS="$2"
export EXPECT_CMD="$1"
export EXPECT_EXPECTED="$3"
/usr/bin/expect <<-'EOF'
set timeout -1
set pass $env(EXPECT_PASS)
set cmd $env(EXPECT_CMD)
set expected_str $env(EXPECT_EXPECTED)
spawn {*}$cmd
expect {
"*yes/no" { send "yes\r"; exp_continue }
-nocase "*The password is incorrect*" { exit 1 }
-nocase "*The password can only contain*" { exit 1 }
-nocase "*assword:" { send "$pass\r"; exp_continue }
-nocase "*password*" { send "$pass\r"; exp_continue }
-nocase "*Please enter password*" { send "$pass\r"; exp_continue }
-nocase "*Notice*" { exp_continue }
-glob "*$expected_str*" { }
eof { }
}
EOF
local ret=$?
unset EXPECT_PASS EXPECT_CMD EXPECT_EXPECTED
return $ret
}
function expect_hostname()
{
export EXPECT_PASS="$2"
export EXPECT_CMD="$1"
expect <<-'EOF' > expectFile
set timeout -1
set pass $env(EXPECT_PASS)
set cmd $env(EXPECT_CMD)
spawn {*}$cmd
expect {
"*yes/no" { send "yes\r"; exp_continue }
-nocase "*assword:" { send "$pass\r"; exp_continue }
-nocase "*password*" { send "$pass\r"; exp_continue }
eof { }
}
EOF
local ret=$?
unset EXPECT_PASS EXPECT_CMD
return $ret
}
validate_username() {
local username=$1
if [[ ! $username =~ ^[a-zA-Z_][a-zA-Z0-9_]{0,31}$ ]]; then
die "Invalid username: only letters, numbers and underscores are allowed, length 1-32, cannot start with a number"
fi
}
validate_groupname() {
local groupname=$1
if [[ ! $groupname =~ ^[a-zA-Z_][a-zA-Z0-9_]{0,31}$ ]]; then
die "Invalid group name: only letters, numbers and underscores are allowed, length 1-32, cannot start with a number"
fi
}
validate_host_ips() {
local ips=$1
local ip_arr=(${ips//,/ })
if [ ${#ip_arr[@]} -ne 2 ]; then
die "Invalid host count: this script only supports 2-node deployment (one master, one slave)"
fi
for ip in "${ip_arr[@]}"; do
if [[ ! $ip =~ ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ ]]; then
die "Invalid IPv4 address format: ${ip}"
fi
done
}
# Verify valid port range
# Valid port: 1024-65535, numeric only
validate_port() {
local port=$1
if [[ ! $port =~ ^[0-9]+$ ]] || [ $port -lt 1024 ] || [ $port -gt 65535 ]; then
die "Invalid port: must be a number between 1024 and 65535"
fi
}
# Verify valid installation path
# Absolute path only, no special injection characters
validate_install_path() {
local path=$1
# Check it's an absolute path and doesn't have double slashes
if [[ ! $path = /* || $path == *//* ]]; then
die "Invalid installation path: absolute path only, no special/suspicious characters"
fi
# Check for forbidden characters
if [[ $path == *';'* || $path == *'&'* || $path == *'|'* || $path == *'`'* || $path == *'$'* || $path == *'('* || $path == *')'* || $path == *'['* || $path == *']'* || $path == *'{'* || $path == *'}'* ]]; then
die "Invalid installation path: absolute path only, no special/suspicious characters"
fi
}
# Verify password complexity and security (暂时禁用)
validate_password() {
local pwd=$1
# 暂时跳过所有密码校验,直接通过
return 0
}
# Verify XML template file path
# Ensure file exists and no suspicious characters
validate_xml_path() {
local xml=$1
if [[ ! -f $xml ]]; then
die "Invalid XML path: file not exists or contains suspicious characters"
fi
# Check for forbidden characters
if [[ $xml == *';'* || $xml == *'&'* || $xml == *'|'* || $xml == *'`'* || $xml == *'$'* || $xml == *'('* || $xml == *')'* || $xml == *'['* || $xml == *']'* || $xml == *'{'* || $xml == *'}'* ]]; then
die "Invalid XML path: file not exists or contains suspicious characters"
fi
}
function main()
{
while [ $# -gt 0 ]
do
case "$1" in
-h|--help)
print_help
exit 1
;;
-U|--user_name)
if [ "$2"X = X ]
then
die "no cluster user values"
fi
USER_NAME=$2
validate_username "${USER_NAME}"
shift 2
;;
-G|--user_grp)
if [ "$2"X = X ]
then
die "no group values"
fi
USER_GROUP=$2
validate_groupname "${USER_GROUP}"
shift 2
;;
-H|--host_ip)
if [ "$2"X = X ]
then
die "no intranet ip address of the host values"
fi
HOST_IPS=$2
shift 2
HOST_IPS_ARR=${HOST_IPS//,/ }
HOST_IPS_ARRAY=(${HOST_IPS_ARR})
if [ ${#HOST_IPS_ARRAY[*]} != 2 ]
then
die "the current script can be installed only on two nodes, one active node and one standby node"
fi
;;
-X|--xml_location)
if [ "$2"X = X ]
then
die "no cluster xml configuration file values"
fi
XML_DIR=$2
validate_xml_path "${XML_DIR}"
shift 2
;;
-D|--install_location)
if [ "$2"X = X ]
then
die "no installation directory of the openGauss program values"
fi
INSTALL_PATH=$2
validate_install_path "${INSTALL_PATH}"
shift 2
;;
-p|--port)
if [ "$2"X = X ]
then
die "the port number cannot be empty."
fi
PORT=$2
validate_port "${PORT}"
shift 2
;;
-P|--password)
if [ "$2"X = X ]
then
die "the password cannot be empty."
fi
PASSWORD="$2"
validate_password "${PASSWORD}"
shift 2
;;
*)
echo "Internal Error: option processing error" 1>&2
echo "please input right paramtenter, the following command may help you"
echo "sh active_standby_nodes_install.sh --help or sh active_standby_nodes_install.sh -h"
exit 1
esac
done
if [ "${USER_NAME}"X == X ]
then
die "no cluster user values"
fi
if [ -z ${INSTALL_PATH} ]
then
INSTALL_PATH="/home/${USER_NAME}"
fi
if [ "${PASSWORD}"X == X ]
then
echo "please enter the password of the root user&the password of a common user(the two passwords must be the same)"
echo -n "password:"
read PASSWORD
while [ -z ${PASSWORD} ]
do
echo "the value cannot be null, please enter the password again"
echo -n "password:"
read PASSWORD
done
fi
if [ "${HOST_IPS}"X == X ]
then
die "no intranet ip address values"
else
len=${#HOST_IPS_ARRAY[*]}
index=0
while [ ${index} -lt ${len} ]
do
expect_hostname "ssh ${HOST_IPS_ARRAY[${index}]} hostname" ${PASSWORD}
if [ $? == 0 ]
then
expectResult=$(tail -1 expectFile|head -1| tr -d "\r"| tr -d "\n")
if [ -z ${expectResult} ]
then
die "failed to obtain the hostname based on the ip address of ${HOST_IPS_ARRAY[${index}]}."
fi
HOST_NAMES_ARRAY[${index}]=${expectResult}
else
die "failed to obtain the hostname based on the ip address of ${HOST_IPS_ARRAY[${index}]}."
fi
index=$[ ${index} + 1 ]
done
fi
rm -rf expectFile
HOST_NAMES="${HOST_NAMES_ARRAY[0]},${HOST_NAMES_ARRAY[1]}"
SYSTEM_ARCH=`uname -p`
SYSTEM_NAME=`cat /etc/*-release | grep '^ID=".*'|awk -F "[=\"]" '{print $3}'`
if [ "${SYSTEM_NAME}" == "openEuler" ] && [ "${SYSTEM_ARCH}" == "aarch64" ]
then
info "the current system environment is openEuler + arm"
elif [ "${SYSTEM_NAME}" == "openEuler" ] && [ "${SYSTEM_ARCH}" == "x86_64" ]
then
info "the current system environment is openEuler + x86"
elif [ "${SYSTEM_NAME}" == "centos" ] && [ "${SYSTEM_ARCH}" == "x86_64" ]
then
info "the current system environment is CentOS + x86"
elif [ "${SYSTEM_NAME}" == "redhat" ] && [ "${SYSTEM_ARCH}" == "x86_64" ]
then
info "the current system environment is redhat + x86"
elif [ "${SYSTEM_NAME}" == "redhat" ] && [ "${SYSTEM_ARCH}" == "aarch64" ]
then
info "the current system environment is redhat + arm"
elif [ "${SYSTEM_NAME}" == "kylin" ] && [ "${SYSTEM_ARCH}" == "x86_64" ]
then
info "the current system environment is kylin + x86"
elif [ "${SYSTEM_NAME}" == "kylin" ] && [ "${SYSTEM_ARCH}" == "aarch64" ]
then
info "the current system environment is kylin + arm"
else
warn "the current system environment is ${SYSTEM_NAME} + ${SYSTEM_ARCH}, \
you are advised to use the centos, openEuler, redhat, or kylin system. because OpenGauss may not adapt to the current system."
fi
validate_username "${USER_NAME}"
validate_groupname "${USER_GROUP}"
validate_host_ips "${HOST_IPS}"
validate_port "${PORT}"
validate_install_path "${INSTALL_PATH}"
validate_password "${PASSWORD}"
validate_xml_path "${XML_DIR}"
info "installation parameter verification completed."
}
function checks()
{
system_arch=`uname -p`
system_name=`cat /etc/*-release | grep '^ID=".*'|awk -F "[=\"]" '{print $3}'`
if [ ${system_arch} != "$8" -o ${system_name} != "$9" ]
then
warn "inconsistency between the system and the execution machine"
fi
egrep "^$3" /etc/group >& /dev/null
if [ $? != 0 ];then
groupadd $3
fi
egrep "^$4" /etc/passwd >& /dev/null
if [ $? != 0 ];then
useradd -g $3 -d /home/$4 -m -s /bin/bash $4 2>/dev/null
if [ $? != 0 ]
then
die "failed to create the user on the node $2."
fi
expect_ssh "passwd $4" "$5" "passwd:"
if [ $? != 0 ]
then
die "an error occurred when setting the user password on the node $2"
fi
fi
sed -i "s/SELINUX=.*/SELINUX=disabled/g" /etc/selinux/config && firewall-cmd --permanent --add-port="$6/tcp" && firewall-cmd --reload
if [ $? != 0 ]
then
warn "some errors occur during system environment setting on host $2"
fi
umask 0022
INSTALL_PATH=$7
if [ ! -e ${INSTALL_PATH} ]
then
mkdir -p ${INSTALL_PATH}
else
rm -rf ${INSTALL_PATH}/*
fi
chmod -R 755 ${INSTALL_PATH}/
chown -R $4:$3 ${INSTALL_PATH}/
if [ -f /${10} ]
then
USER_HOME=$(getent passwd "$4" | cut -d: -f6)
if [ -z "${USER_HOME}" ]; then
die "failed to get the home directory of user $4"
fi
mv /${10} "$USER_HOME"/
fi
echo "check end"
}
function pre_checks()
{
if [ ${#HOST_IPS_ARRAY[*]} == 0 ]
then
die "the number of internal IP addresses of the host is incorrect."
fi
localips=$(/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:")
for ip in ${HOST_IPS_ARRAY[@]}
do
info "start to check the installation environment of host ${ip}."
sleep 2s
# standby node
if [[ $localips != *${ip}* ]]
then
sshcmd="scp ${SCRIPT_PATH}/${SCRIPT_NAME} root@${ip}:/"
expect_ssh "${sshcmd}" "${PASSWORD}" "100%"
if [ $? != 0 ]
then
die "an error occurred when copying the script to the target host ${ip}."
fi
# 使用数组和 printf %q 安全构建远程命令
# 将所有参数放入数组,然后用 printf %q 统一转义,避免引号嵌套问题
local -a remote_cmd_args=(
"sh" "/${SCRIPT_NAME}" "inner" "${ip}"
"${USER_GROUP}" "${USER_NAME}" "${PASSWORD}"
"${PORT}" "${INSTALL_PATH}" "${SYSTEM_ARCH}"
"${SYSTEM_NAME}" "${SCRIPT_NAME}"
)
local remote_cmd=$(build_safe_command "${remote_cmd_args[@]}")
# 使用双引号确保远程命令作为一个整体传递给 SSH
sshcmd="ssh ${ip} ${remote_cmd}"
expect_ssh "${sshcmd}" "${PASSWORD}" "check end"
if [ $? != 0 ]
then
die "an error occurred during the pre-installation check on the target host ${ip}."
fi
else
# local
checks "" ${ip} ${USER_GROUP} ${USER_NAME} "${PASSWORD}" ${PORT} "${INSTALL_PATH}" ${SYSTEM_ARCH} ${SYSTEM_NAME} ${SCRIPT_NAME}
if [ $? != 0 ]
then
die "an error occurred during the pre-installation check on the target host ${ip}."
fi
fi
info "succeeded in checking the installation environment of host ${ip}."
done
return 0
}
function xmlconfig()
{
info "start to automatically configure the installation file."
# 使用 awk 替代 sed,避免特殊字符问题
local tmp_xml=$(mktemp)
if [ ! -e ${XML_DIR} ]
then
rm -f "$tmp_xml"
die "cannot find one_master_one_slave_template.xml in ${XML_DIR}"
fi
# 使用 awk 进行安全替换
awk -v nodeNames="${HOST_NAMES}" \
-v backIpls="${HOST_IPS}" \
-v clusterName="${USER_NAME}" \
-v port="${PORT}" \
-v installPath="${INSTALL_PATH}" \
-v nodeName1="${HOST_NAMES_ARRAY[0]}" \
-v backIp1="${HOST_IPS_ARRAY[0]}" \
-v nodeName2="${HOST_NAMES_ARRAY[1]}" \
-v backIp2="${HOST_IPS_ARRAY[1]}" \
'{
gsub(/@{nodeNames}/, nodeNames);
gsub(/@{backIpls}/, backIpls);
gsub(/@{clusterName}/, clusterName);
gsub(/@{port}/, port);
gsub(/@{installPath}/, installPath);
gsub(/@{nodeName1}/, nodeName1);
gsub(/@{backIp1}/, backIp1);
gsub(/@{nodeName2}/, nodeName2);
gsub(/@{backIp2}/, backIp2);
print;
}' "${XML_DIR}" > "$tmp_xml"
local awk_status=$?
# 校验 awk 执行成功且生成的 XML 文件非空
if [ $awk_status -ne 0 ] || [ ! -s "$tmp_xml" ]; then
rm -f "$tmp_xml"
die "failed to generate the installation XML file (awk exit status: ${awk_status})"
fi
USER_HOME=$(getent passwd "${USER_NAME}" | cut -d: -f6)
if [ -z "${USER_HOME}" ]; then
rm -f "$tmp_xml"
die "failed to get the home directory of user ${USER_NAME}"
fi
mv "$tmp_xml" "$USER_HOME/one_master_one_slave.xml"
if [ $? -ne 0 ]; then
rm -f "$tmp_xml"
die "failed to move the generated XML file to ${USER_HOME}/one_master_one_slave.xml"
fi
cat "$USER_HOME/one_master_one_slave.xml"
info "the installation file is automatically configured"
return 0
}
function install()
{
info "preparing for preinstallation"
home_path=$(getent passwd "${USER_NAME}" | cut -d: -f6)
export LD_LIBRARY_PATH="${PACKAGE_PATH}/script/gspylib/clib:"$LD_LIBRARY_PATH
sshcmd="python3 "${PACKAGE_PATH}"/script/gs_preinstall -U "${USER_NAME}" \
-G "${USER_GROUP}" -X "${home_path}"/one_master_one_slave.xml --sep-env-file="${home_path}"/env_master_slave --skip-os-set"
info "cmd \"${sshcmd}\""
expect_ssh "${sshcmd}" "${PASSWORD}" "Preinstallation succeeded"
if [ $? != 0 ]
then
die "preinstall failed."
fi
info "preinstallation succeeded."
chmod 755 ${home_path}'/one_master_one_slave.xml'
chown ${USER_NAME}:${USER_GROUP} ${home_path}'/one_master_one_slave.xml'
info "start the installation."
su - ${USER_NAME} -c"source ${home_path}/env_master_slave;gs_install -X ${home_path}/one_master_one_slave.xml;gs_om -t status --detail"
if [ $? -ne 0 ]
then
die "install failed."
else
info "install success."
fi
exit 0
}
if [ "$1" == "inner" ]
then
# 在 inner 模式下,确保所有参数都正确传递
checks "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "${10}"
else
main "$@"
pre_checks
xmlconfig
install
fi
exit 0