“@server1 @server2 restart tomcat” --- 以Twitter(微博)的语法风格执行ssh、scp命令

日常工作中,如果经常需要从工作机登录到其他机器或服务器,在其他机器批量执行命令,或使用scp批量上传/下载文件的话,往往需要敲多次重复冗长的ssh命令。例如:

# 登录某台机器
ssh deploy@192.168.1.200
ssh tomcat@192.169.2.150

# 批量执行某个命令
ssh admin@192.168.1.200 hbase-daemon.sh restart regionserver
ssh admin@192.168.1.201 hbase-daemon.sh restart regionserver
ssh admin@192.168.1.202 hbase-daemon.sh restart regionserver

# 上传某个文件夹
scp -r classes tomcat@192.168.1.150:/home/tomcat_base/WEB-INF/
scp -r classes tomcat@192.168.1.151:/home/tomcat_base/WEB-INF/

# 下载某个文件夹
scp -r admin@192.168.1.200:/user/local/hbase/conf ./

其实,在目标机器的地址和用户名均已知的情况下,若能通过给它们预设一些别名,并采用Twitter(微博)的“圈人”方式执行ssh或scp命令,则会十分的快速方便。即:

# 登录某台机器。直接@该机器的别名
@200
@tomcat1

# 批量执行某个命令。@各机器的别名并跟上要执行的命令
@hbase1 @hbase2 @hbase3 hbase-daemon.sh restart regionserver

# 上传某个文件夹。@各机器的别名,加上u表示上传(Upload),再跟上本地路径和远程路径
@tomcat1 @tomcat2 u classes /home/tomcat_base/WEB-INF/

# 下载某个文件夹。@机器的别名,加上d表示下载(Download),再跟上远程路径和本地路径
@hbase1 d /user/local/hbase/conf ./

如何实现以上的语法?要知道尝试在shell执行以@开头的命令肯定会报错,例如:

$ @123
-bash: @123: command not found

但Bash默认使用一个函数command_not_found_handle来处理找不到的命令并对用户进行提示。我们只要修改这个函数,就能让shell在遇到以@开头的命令时,执行ssh或scp命令。

以Ubuntu 14.04为例,报错函数位于/etc/bash.bashrc,定义如下:

        function command_not_found_handle {
                # check because c-n-f could've been removed in the meantime
                if [ -x /usr/lib/command-not-found ]; then
                   /usr/lib/command-not-found -- "$1"
                   return $?
                elif [ -x /usr/share/command-not-found/command-not-found ]; then
                   /usr/share/command-not-found/command-not-found -- "$1"
                   return $?
                else
                   printf "%s: command not found\n" "$1" >&2
                   return 127
                fi
        }

可以看到该函数在找不到命令时会去某些位置寻找合适的错误处理程序,找到的话就进行调用,否则就简单地报错。

在该函数开头拦截以@开头的命令,并调用ssh命令即可实现我们想要的逻辑:

        function command_not_found_handle {

                # 插入代码
                # 如果命令以@开头
                if [[ $1 =~ ^@.* ]]; then
                   # 调用我们的ssh脚本at.sh
                   at.sh "$@"
                   # 返回
                   return 0
                fi
                # 结束插入代码

                # check because c-n-f could've been removed in the meantime
                if [ -x /usr/lib/command-not-found ]; then
                   /usr/lib/command-not-found -- "$1"
                   return $?
                elif [ -x /usr/share/command-not-found/command-not-found ]; then
                   /usr/share/command-not-found/command-not-found -- "$1"
                   return $?
                else
                   printf "%s: command not found\n" "$1" >&2
                   return 127
                fi
        }

接下来只要实现我们的ssh脚本at.sh即可。我的实现如下:

# 配置各机器的别名。每行一台机器,语法为“别名1|别名2|别名3...=用户名@地址”,当机器存在多个别名时,可以通过@任意一个别名访问该机器
presets="
200|hbase1=service@192.168.1.200
201|hbase2=service@192.168.1.201
202|hbase3=service@192.168.1.202
150|tomcat1=service@192.168.2.150
151|tomcat2=service@192.168.2.151
"
# 存储用户@到的别名列表
targets=()
# 是否执行scp(false视为执行ssh)
scp=false
# 存储ssh待执行的命令
ssh_cmd=""
# 当执行scp时,是否是要下载文件(false视为上传)
scp_down=false
# 执行scp时的源文件路径
scp_a=""
# 执行scp时的目标文件路径
scp_b=""
# 根据用户@到的别名查找机器,返回“用户名@地址”形式的机器地址,找不到则返回空字符串
function find_preset() {
  IFS=$'\n'
  for preset_line in $presets; do
    IFS='=' read -a splitted_preset_line <<< "$preset_line"
    preset_aliases="${splitted_preset_line[0]}"
    preset="${splitted_preset_line[1]}"
    if [[ "|${preset_aliases}|" = *\|$1\|* ]]; then
      echo "$preset"
      return
    fi
  done
}
# 遍历命令行参数
for ((i=1;i<=$#;i++)); do
  # 如果不以@开头
  if ! [[ ${!i} =~ ^@.* ]]; then
    # 如果别名列表后遇到“u”或者“d”,意味着用户要使用scp
    if [[ ${!i} = "u" ]] || [[ ${!i} = "d" ]]; then
      # 设scp标识
      scp=true
      # 如果是下载
      if [[ ${!i} = "d" ]]; then
        # 设下载标识
        scp_down=true
      fi
      # 把后面两个参数存到scp的源文件路径和目标文件路径中去
      i=$(expr $i + 1)
      scp_a="${!i}"
      i=$(expr $i + 1)
      scp_b="${!i}"
    # 否则是ssh调用
    else
      # 把后续的所有参数拼接并存储到ssh命令变量中
      ssh_cmd="${!i}"
      for ((j=i+1;j<=$#;j++)); do
        ssh_cmd="$ssh_cmd ${!j}"
      done
    fi
    break
  fi
  # 去掉参数开头的“@”
  target_with_at="${!i}"
  # 存储到目标别名列表
  targets[i]="${target_with_at:1}"
done
# 遍历所有目标别名
for target in ${targets[*]}; do
  # 根据目标别名查找机器
  preset=$(find_preset $target)
  # 找不到则报错并跳过该别名
  if ! [ -n "$preset" ]; then
    echo -e "\e[31mUnknown target: $target \e[0m"
    continue
  fi
  # 如果是scp调用
  if $scp; then
    @ 如果是下载
    if $scp_down; then
      # 执行scp下载
      scp -r "$preset:$scp_a" "$scp_b"
    # 否则是上传
    else
      # 执行scp上传
      scp -r "$scp_a" "$preset:$scp_b"
    fi
  # 否则是ssh调用
  else
    # 提示用户正在连接
    echo -e "\e[32mRequesting $preset... \e[0m"
    # 执行ssh命令
    ssh "$preset" "$ssh_cmd"
    # 如果待执行命令为空,意味着用户是要直接登录该机器,脚本直接退出
    if ! [ -n "$ssh_cmd" ]; then
      break
    fi
  fi
done

最后只要把at.sh放到PATH中去即可。

你可能感兴趣的:(“@server1 @server2 restart tomcat” --- 以Twitter(微博)的语法风格执行ssh、scp命令)