一、背景:
如果需要实现这样一个功能:
有一个存储机器列表的文件,里面按TAB键分割,依次存储了“IP”,“_USER(能执行ssh后的操作)”,“PORT(ssh协议端口)”这几列,依次登录到每台机器上,拷贝可执行业务代码包,并自动设置该用户(_USER)的crontab定时调度任务,以使任务可以按规定在每台server上启动起来。
则将需求分解,可得到如下操作步骤:
1、读取server list文件(已TAB分割),从里面获取每一行记录,解析出“IP”,“_USER(能执行ssh后的操作)”,“PORT(ssh协议端口)”;
‘’
2、根据1中解析出的server信息,遍历每一台server,ssh远程登录过去,执行如下操作 list:
A). 拷贝业务代码包(biz.tar.gz)到该server,修改文件宿主(由com_admin -> _USER)。
B). 判断该用户的crontab文件(_USER_CRON_FILE)是否已存在,不存在,则由root创建之并将文件所属关系重新修改为_USER。
C). 添加crontab调度业务逻辑到该用户的crontab文件(_USER_CRON_FILE)中。
D). 切换到_USER下,解压业务代码包(biz.tar.gz)。
E). 退出当前shell环境,释放连接
Pre-Requirements:
1、脚本所在的server(如跳板机),需要能通过指定的PORT免密登陆到目标主机;
2、目标主机需要登录root的sudo权限,如本例中的com_admin用户,必须要能通过"sudo su"命令切换为root;
二、代码逻辑
1、服务器列表文件
以TAB作为分隔符,存贮“IP”,“_USER(能执行ssh后的操作)”,“PORT(ssh协议端口)”这几列。
server_list.txt
#111.111.111.111 mysql 22
111.111.111.112 oracle 22
111.111.111.113 com_admin 22222
111.111.111.114 zhangsan 22223
2、业务逻辑 Shell 脚本
biz_runwith_noroot.sh
#!/bin/bash
set -x
# 获取当前路径
dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
SERVER_LIST_FILE=$dir/server_list.txt
echo 'Current dir='$dir
PRESSURE_CODE=biz.tar.gz
# 操作数量
cnt=1
all_cnt=`cat $SERVER_LIST_FILE | wc -l`
# 遍历每一台server
while read line
do
#sleep 10
echo ">>当前正在操作第 " $cnt " 台主机,共有 "$all_cnt " 台主机"
let cnt=cnt+1
#正则表达式,匹配以#开始的行
if [[ "$line" =~ ^#.* ]];then
echo '正在处理的行是:'$line
continue
fi
#!如果处理的数据文件,字段间分割符不是空白字符(如tab、空白)。
# 以csv文件为例,以下注释需要放开,并需临时设置当前IFS=',',并在数据处理完毕后后改回来
# --------------------------------------------------------------------------
#OLD_IFS=$IFS
#IFS=' '
#arr=($line)
# 将每一行按默认IFS(一般指代空白,如tab,或空格)分割,并转化为数组
arr=($line)
#IFS=$OLD_IFS
# 提取IP/_USER属性
IP=${arr[0]}
_USER=${arr[1]}
PORT=${arr[2]}
echo 'IP='${arr[0]}', _USER='${arr[1]}', PORT='${arr[2]}
# 传输加压文件至每一台server上
scp -P $PORT $dir/$PRESSURE_CODE com_admin@${IP}:~
# 每一台server的Home目录
HOME_DIR=/home/${_USER}
# 每一台server上的cron文件,该文件存在于/var/spool/cron/目录下,且以用户名命名
# 如:/var/spool/cron/zhangsan
_USER_CRON_FILE=/var/spool/cron/$_USER
# 创建指定用户(如com_admin、mysql、oracle)的crontab定时调度任务
# 这里使用EOF方式,在ssh登录到主机后,执行一连串操作。
# EOF表示END OF FILE,只是界定处于EOF块之前的操作语句,会被整体执行。
# EOF只是个标识,也可以使用其他成对出现的字符。
ssh -p $PORT $IP << EOF
# 以下操作以root身份执行
sudo su root
cd
pwd
mv /home/com_admin/$PRESSURE_CODE $HOME_DIR/
sleep 3
chown -R $_USER:$_USER $HOME_DIR/${PRESSURE_CODE}
ls -l $HOME_DIR/${PRESSURE_CODE}
# 校验$_USER 用户的cron文件是否已存在。
# 已存在表明之前根据"cron -e"创建过,则cron文件无须更改权限;否则表明用户的cron为手工创建,需修改用户的 cron为系统默认的600
# ------------------------------------------------------------------------------
if [ ! -f "$_USER_CRON_FILE" ];then
echo '不存在文件: '$_USER_CRON_FILE',将创建之,并把该文件权限修改为cron默认的600'
touch $_USER_CRON_FILE
sleep 3
chmod 600 $_USER_CRON_FILE
sleep 3
chown -R $_USER:$_USER $_USER_CRON_FILE
sleep 3
else
echo '已存在文件: '$_USER_CRON_FILE',不做任何操作,维持原文件权限'
fi
cat >> /var/spool/cron/${_USER} << DSG
# 添加crontab调度业务逻辑
0 0 * * * sh /home/${_USER}/biz/start_new.sh main >> /home/${_USER}/biz/start_new.sh.out 2>&1 &
0 5 * * * sh /home/${_USER}/biz/start_new.sh stopAll >> /home/${_USER}/biz/start_new.sh.stopall_out 2>&1 &
DSG
# 切换回 $_USER,较为安全的执行命令
su $_USER
cd ~
tar -xzvf $HOME_DIR/$PRESSURE_CODE
# 如下exit语句,将依次退出各个用户的shell环境,最终关闭整个ssh进程,不空占句柄数,是一种良好的编程习惯。
# 退出当前用户shell(如oracle)
pwd
exit
# 退出当前用户shell(如root)
pwd
exit
# 退出当前用户shell(如com_admin)
pwd
exit
EOF
done < $SERVER_LIST_FILE
echo '已拷贝的服务器个数为:' $(($cnt - 1))
set +x
biz_runwith_comadmin.sh
#!/bin/bash
echo "The numbers of input parameters is: "$#
echo "The script's input parameters is: "$@
echo ""
if [[ $# -ne 4 ]];then
echo "USAGE: $0 <SSH_PORT> <start|stop> <vm|phy> <1|2|3>"
echo "Please confirm your input parameters."
exit
else
server_type=$3
mem_type=$4
if [ $4 = 1 ];then
mem_type="group_23g"
elif [ $4 = 2 ];then
mem_type="group_30g"
elif [ $4 = 3 ];then
mem_type="group_38g"
fi
fi
dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
SERVER_LIST_FILE=$dir/server_list.txt.${server_type}.${mem_type}
echo $dir
PRESSURE_CODE=my_order.tar.gz
PRESSURE_PROJECT_NAME=my_order
# 操作数量
cnt=1
all_cnt=`cat $SERVER_LIST_FILE | wc -l`
# 遍历每一台server
while read line
do
echo ">>当前正在操作第 " $cnt " 台主机,共有 "$all_cnt " 台主机"
#echo $line
#过滤掉#开头的IP机器
if [[ "$line" =~ ^#.* ]];then
echo '正在处理的行是:'$line
continue
fi
#OLD_IFS=$IFS
#IFS=' '
#arr=($line)
# 将每一行按默认IFS(一般指代空白,如tab,或空格)分割,并转化为数组
arr=($line)
#IFS=$OLD_IFS
# 提取IP/USER属性
IP=${arr[0]}
USER=${arr[1]}
#PORT=${arr[2]}
PORT=$1
COMMAND=$2
#echo 'IP='${arr[0]}', USER='${arr[1]}', PORT='${arr[2]}
echo 'IP='$IP', USER='$USER', PORT='$PORT
# transfer PRESSURE file to each server
scp -P $PORT $dir/$PRESSURE_CODE com_admin@${IP}:~
# create user's crontab(eg. mysql、oracle、bi_admin)
ssh -p $PORT $IP << EOF
sudo su root
cd
pwd
# Copy my_order add pression program to root's home directory.
mv /home/com_admin/$PRESSURE_CODE ~
ls -l $PRESSURE_CODE
tar -xzvf $PRESSURE_CODE
#cat >> /var/spool/cron/root << DSG
#50 19 * * * sh /root/my_order/start_new.sh main >> /root/my_order/start_new.sh.out 2>&1 &
#59 19 * * * sh /root/my_order/start_new.sh stopAll >> /root/my_order/start_new.sh.stopall_out 2>&1 &
#DSG
#chmod 600 /var/spool/cron/root
chown -R root. $PRESSURE_PROJECT_NAME
cd $PRESSURE_PROJECT_NAME
#Decompression my_order/jdk-8u191-linux-x64.tar.gz
#rm -rf jdk1.8.0_191
JDK_DIR=/root/jdk1.8.0_191
#不存在和有执行权限的话,从my_order文件夹中拷贝到root根下,并解压;也可以用"-d"直接判断是否为folder
if [ ! -x $JDK_DIR ];then
cp jdk-8u191-linux-x64.tar.gz /root
tar -xzvf /root/jdk-8u191-linux-x64.tar.gz -C /root/
fi
if [[ ${COMMAND} =~ "stop" ]];then
nohup sh /root/my_order/start_new.sh stopAll >> /root/my_order/start_new.sh.out 2>&1 &
elif [[ ${COMMAND} =~ "start" ]];then
nohup sh /root/my_order/start_new.sh main >> /root/my_order/start_new.sh.out 2>&1 &
fi
exit
EOF
let cnt=cnt+1
done < $SERVER_LIST_FILE
echo '已拷贝的BI服务器个数为:' $(($cnt - 1))
#Exit this program
exit
三、总结
1、避开系统变量为变量命名。
如USER替换为_USER
2、shell中获取一个变量讲过加减乘除后的值,可以使用如下语法:
①、let cnt=cnt+1
②、$(($cnt - 1))
3、精准获取脚本执行的当前上下文路径:
dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
4、遍历并读取文件中的每一行,可以使用如下语法:
while read line
do
。。。
done < $SERVER_LIST_FILE
5、判断字符串是否以某指定字符开头,以下以#开头为例说明:
if [[ "$line" =~ ^#.* ]];then
continue
fi
continue:退出本次操作,进入下一个循环操作;
break:退出本层循环,如果循环有多层,可以使用break n
语法。
6、正确使用IFS进行字符分割
如果字符串分割符不是默认的空白(如tab,或空格),则需要使用这种语法进行字符串分割,操作步骤如下:
①、保存系统IFS;
②、IFS赋值为新的字段分隔符,如解析csv文件,需指定为逗号分割符;
③、使用()语法将字符串按指定分隔符转换成数组array;
④、遍历数组语法:
A)、遍历每一个元素:${arr[0]},${arr[1]},${arr[2]}
B)、遍历全部元素:${arr[@]}
OLD_IFS=$IFS
IFS=' ,'
# 将每一行按默认IFS(一般指代空白,如tab,或空格)分割,并转化为数组
arr=($line)
IFS=$OLD_IFS
7、默认ssh端口不是22时ssh和scp命令的用法:
①、scp (大P)
scp -P $PORT
d
i
r
/
dir/
dir/PRESSURE_CODE com_admin@${IP}:~
②、ssh(小p)
ssh -p $PORT $IP << EOF
8、判断文件存在 vs 判断字符以指定字符开头语法的不同
①、判断文件存在(一个中括号
)
if [ ! -f "$USER_CRON_FILE" ];then
②、判断字符以指定字符开头(两个中括号
)
if [[ "$line" =~ ^#.* ]];then
9、ssh免密配置操作
①、目标主机 “ssh-keygen -t rsa”
②、拷贝跳板机上.ssh中的id_rsa.pub到目标主机的authorized_keys(权限600)
M1:拷贝跳板机id_rsa.pub内容,黏贴到目标主机的authorized_keys文件中。
M2:ssh-copy-id -i ~/.ssh/id_rsa.pub 目标机器ip地址/主机名
10、目标主机上普通用户添加sudo权限操作
①、root身份打开/etc/sudoers文件;
②、"## Same thing without a password" 这行下添加如下语句:
%com_admin ALL=(ALL) NOPASSWD: ALL