「@server1 @server2 restart tomcat」 --- 以Twitter(微博)的語法風格執行ssh、scp命令

平常工做中,若是常常須要從工做機登陸到其餘機器或服務器,在其餘機器批量執行命令,或使用scp批量上傳/下載文件的話,每每須要敲屢次重複冗長的ssh命令。例如:shell

# 登陸某臺機器
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命令,則會十分的快速方便。即:tomcat

# 登陸某臺機器。直接@該機器的別名
@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執行以@開頭的命令確定會報錯,例如:bash

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

但Bash默認使用一個函數command_not_found_handle來處理找不到的命令並對用戶進行提示。咱們只要修改這個函數,就能讓shell在遇到以@開頭的命令時,執行ssh或scp命令。服務器

以Ubuntu 14.04爲例,報錯函數位於/etc/bash.bashrc,定義以下:ssh

        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命令便可實現咱們想要的邏輯:spa

        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中去便可。rest

相關文章
相關標籤/搜索