堡壘機服務器 Intellij IDEA 一鍵自動打包、部署、遠程重啟應用、查看啟動日志


需求

有的公司的服務器使用堡壘機管理,登錄后需要手動選擇堡壘機對應的序號,需求是在 Intellij IDEA 中,使用自己寫的腳本完成一鍵打包,自動登錄堡壘機,自動選擇目標機器,上傳文件,重啟應用,查看日志輸出。

在IDEA中實現自動化部署的幾種方法

所謂自動化部署,就是根據一個觸發條件,自動運行一段腳本(可以是java,python,js,linux shell),這段腳本的作用就是:打包,上傳,ssh登錄,執行啟動腳本,查看輸出。

使用 Alibaba Cloud Toolkit 插件

這個可以參考其他文章學習,不支持堡壘機自動選擇。

使用IDEA自帶的 deployment 工具

參考其他文章,使用這個工具,先打包需要點擊 1-3次,再上傳點擊 2-3 次,再打開ssh 要點擊 2-3次,再輸入命令 2-6 次,十分繁瑣。

使用第三方的Expect工具

如可以參考 https://github.com/Alexey1Gavrilov/ExpectIt#interacting-with-os-process 這個例子上傳文件, 參考 https://github.com/Alexey1Gavrilov/ExpectIt#interacting-with-ssh-server 這個例子實現自動選擇堡壘機,輸入遠程命令。

自己寫一段腳本實現了 Expect (java,python,js,linux shell都可以,本文以java為例)

也就是本文的方法。
IDEA 系列的 IDE 環境,基於一套 action 系統來運行,每一個按鈕,每一個輸入,都會綁定到一個action,執行action的代碼。
因此,我們的想法就是,利用 IDEA 自己的 action 體系,來自動化部署。

開始

編寫部署腳本

這里以Java為例,如果是在linux或mac作為開發環境,推薦使用shell腳本。如果是python開發,寫python更適合。
新建一個Java工程,引入sshj依賴。

   <dependency>
      <groupId>com.hierynomus</groupId>
      <artifactId>sshj</artifactId>
      <version>0.30.0</version>
    </dependency>

新建一個Java文件,這里以 AutoDeploy 為類名(基本完成,可根據需要自行修改)。

import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.LoggerFactory;
import net.schmizz.sshj.common.StreamCopier;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class AutoDeploy {

  private static final String[] requireKeys = {"host", "username", "password", "localFile", "remotePath"};
  private static final String[] notRequireKeys = {"port", "command"};
  static StringBuilder missingKey = new StringBuilder();
  static boolean needRecordStdOut = true;
  static StringBuffer shellOutBuffer;

  public static void main(String[] arg) {
    HashMap<String,String> argMap = setDeployArgs(arg);
    try {
      List<ExpectInput> expectInputList = getExpectInput(arg);

      SSHClient ssh = new SSHClient();
      ssh.addHostKeyVerifier(new PromiscuousVerifier());
      ssh.connect(argMap.get("host"), Integer.parseInt(argMap.getOrDefault("port", "22")));
      ssh.authPassword(argMap.get("username"), argMap.get("password"));

      // SFTPClient sftp = ssh.newSFTPClient();
      // sftp.put(deployArg.localFile, deployArg.remotePath);
      // sftp.close();

      Session session = ssh.startSession();
      if (expectInputList.isEmpty()) {
        // 如果沒有需要手動選擇的跳板機, 直接執行命令即可,
        Session.Command cmd = session.exec(argMap.getOrDefault("command", "ls"));
        String ret = IOUtils.readFully(cmd.getInputStream()).toString();
        System.out.println(ret);
        return;
      }

      // 如果類似堡壘機的, 需要手動選擇, 使用下面的方法, 根據正則表達式自動選擇

      shellOutBuffer = new StringBuffer(8192);
      session.allocateDefaultPTY();
      Session.Shell shell = session.startShell();
      new Thread(new SysOutThread(shell.getInputStream())).start();

      OutputStream output = shell.getOutputStream();
      // 按自定義的正則輸入字符 todo 抽象出  expect() 函數
      for (ExpectInput expectInput : expectInputList) {
        expectStdOut(Pattern.compile(expectInput.expect, Pattern.DOTALL));
        String input = findInputFromExceptBefore(expectInput);
        output.write(input.getBytes());
        output.flush();
      }

      if (argMap.get("command") != null) {
        output.write((argMap.get("command") + "\n").getBytes());
        output.flush();
      }

      // Now make System.in act as stdin. To exit, hit Ctrl+D (since that results in an EOF on System.in)
      // This is kinda messy because java only allows console input after you hit return
      // But this is just an example... a GUI app could implement a proper PTY
      new StreamCopier(System.in, output, LoggerFactory.DEFAULT)
          .bufSize(shell.getRemoteMaxPacketSize())
          .copy();

      session.close();
      ssh.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * TODO 添加超時參數, 超時后退出循環
   */
  private static void expectStdOut(Pattern pattern) throws InterruptedException {
    int size = 0;
    while (true) {
      // 和上個列表一樣,sleep 1 s
      if (size == shellOutBuffer.length()) {
        Thread.sleep(500);
        continue;
      }

      size = shellOutBuffer.length();
      if (pattern.matcher(shellOutBuffer).matches()) {
        Thread.sleep(500);
        return;
      }
    }
  }


  private static String findInputFromBeforeByPattern(Pattern pattern) {
    Matcher matcher = pattern.matcher(shellOutBuffer);
    if (matcher.matches()) {
      String input = matcher.group(1) + "\n";
      shellOutBuffer.delete(0, shellOutBuffer.length());
      return input;
    } else {
      throw new IllegalArgumentException("未找到符合" + pattern.toString() + "的字符");
    }
  }


  private static String findInputFromExceptBefore(ExpectInput expectInput) {
    Pattern expectHasGroup = Pattern.compile(".*\\(.*\\).*", Pattern.DOTALL);
    if (expectHasGroup.matcher(expectInput.expect).matches()) {
      return findInputFromBeforeByPattern(Pattern.compile(expectInput.expect, Pattern.DOTALL));
    } else if (expectHasGroup.matcher(expectInput.input).matches()) {
      return findInputFromBeforeByPattern(Pattern.compile(expectInput.expect, Pattern.DOTALL));
    } else {
      shellOutBuffer.delete(0, shellOutBuffer.length());
      return expectInput.input.contains("\n") ? expectInput.input : expectInput.input + "\n";
    }
  }


  private static List<ExpectInput> getExpectInput(String[] arg) {
    List<ExpectInput> expectInputList = new ArrayList<>();
    for (String s : arg) {
      int expectIndexInString, inputIdxInMainArgs;
      if (s.startsWith("expect") && (expectIndexInString = s.indexOf("=")) >= 0) {
        ExpectInput expectInput = new ExpectInput();
        expectInput.expect = s.substring(expectIndexInString + 1);
        String groupNum = s.substring(6, expectIndexInString);
        String inputKey = "input" + groupNum + "=";
        if ((inputIdxInMainArgs = indexOf(arg, inputKey)) >= 0) {
          expectInput.input = arg[inputIdxInMainArgs].substring(inputKey.length());
        }
        expectInputList.add(expectInput);
      }
    }
    return expectInputList;
  }


  /**
   * 解析命令行參數, 得到需要的數據
   */
  private static HashMap<String, String> setDeployArgs(String[] arg) {
    if (arg == null || arg.length == 0) {
      throw new IllegalArgumentException("參數為空");
    }
    HashMap<String, String> argsMap = new HashMap<>();
    for (String key : requireKeys) {
      int i = indexOf(arg, key + "=");
      if (i < 0) {
        missingKey.append(key).append(",");
      } else {
        argsMap.put(key, arg[i].substring(key.length() + 1));
      }
    }
    for (String key : notRequireKeys) {
      int i = indexOf(arg, key + "=");
      if (i > 0) {
        argsMap.put(key, arg[i].substring(key.length() + 1));
      }
    }
    if (missingKey.length() != 0) {
      throw new IllegalArgumentException("缺少參數" + missingKey.substring(0, missingKey.length() - 1));
    }
    return argsMap;
  }

  private static int indexOf(String[] arg, String key) {
    for (int i = 0; i < arg.length; i++) {
      if (arg[i].startsWith(key)) {
        return i;
      }
    }
    return -1;
  }

  /**
   * shell 輸出線程, 把shell的信息輸出到 System.out
   */
  static class SysOutThread implements Runnable {

    InputStream input;

    public SysOutThread(InputStream input) {
      this.input = input;
    }

    @Override
    public void run() {
      try {
        final byte[] buffer = new byte[8192];
        int len = -1;
        while ((len = input.read(buffer)) != -1) { // 當等於-1說明沒有數據可以讀取了
          System.out.write(buffer, 0, len);   // 把讀取到的內容寫到輸出流中
          if (needRecordStdOut) {
            shellOutBuffer.append(new String(buffer, 0, len));
          }
        }
      } catch (IOException ignored) {
      }
    }
  }

  static class ExpectInput {
    String expect;
    String input;
  }
}

運行java文件成功,得到 AutoDeploy.class 等5個class文件,得到所需參數

運行這個java文件成功,並且得到運行時的參數(這個參數是為了后面能夠成功運行 AutoDeploy.class文件,如果是python腳本和shell腳本,就不需要這些參數,直接使用.py或.sh文件即可)。
需要的參數可以在 IDEA 中的輸出中看到

如果像圖片中一樣,需要鼠標單擊一下,讓他顯示出來:

我們需要這里的 -classpath參數,例如,我這里的參數,指向本地maven倉庫的jar文件:

-classpath "D:\Program Files (Dev)\Maven\repository\com\hierynomus\sshj\0.30.0\sshj-0.30.0.jar;D:\Program Files (Dev)\Maven\repository\org\bouncycastle\bcprov-jdk15on\1.66\bcprov-jdk15on-1.66.jar;D:\Program Files (Dev)\Maven\repository\org\bouncycastle\bcpkix-jdk15on\1.66\bcpkix-jdk15on-1.66.jar;D:\Program Files (Dev)\Maven\repository\com\jcraft\jzlib\1.1.3\jzlib-1.1.3.jar;D:\Program Files (Dev)\Maven\repository\com\hierynomus\asn-one\0.4.0\asn-one-0.4.0.jar;D:\Program Files (Dev)\Maven\repository\net\i2p\crypto\eddsa\0.3.0\eddsa-0.3.0.jar;D:\Program Files (Dev)\Maven\repository\org\slf4j\slf4j-api\1.7.26\slf4j-api-1.7.26.jar;D:\Program Files (Dev)\Maven\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\Program Files (Dev)\Maven\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;"

然后我們把編譯好的 class文件找到,復制到一個文件夾中,例如我這里是 C:\Users\bpzj\Desktop\auto-deploy 文件夾。
把這個文件夾路徑添加到上面的 classpath 參數,變成了:

-classpath "C:\Users\bpzj\Desktop\auto-deploy;D:\Program Files (Dev)\Maven\repository\com\hierynomus\sshj\0.30.0\sshj-0.30.0.jar;D:\Program Files (Dev)\Maven\repository\org\bouncycastle\bcprov-jdk15on\1.66\bcprov-jdk15on-1.66.jar;D:\Program Files (Dev)\Maven\repository\org\bouncycastle\bcpkix-jdk15on\1.66\bcpkix-jdk15on-1.66.jar;D:\Program Files (Dev)\Maven\repository\com\jcraft\jzlib\1.1.3\jzlib-1.1.3.jar;D:\Program Files (Dev)\Maven\repository\com\hierynomus\asn-one\0.4.0\asn-one-0.4.0.jar;D:\Program Files (Dev)\Maven\repository\net\i2p\crypto\eddsa\0.3.0\eddsa-0.3.0.jar;D:\Program Files (Dev)\Maven\repository\org\slf4j\slf4j-api\1.7.26\slf4j-api-1.7.26.jar;D:\Program Files (Dev)\Maven\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\Program Files (Dev)\Maven\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;"

因為 sshj 這里使用是logback作為日志,而logback默認的日志級別是 debug,會打印很多沒用的日志信息,所以,我添加了一個logback.xml,再使用參數

-Dlogback.configurationFile=C:\Users\bpzj\Desktop\auto-deploy\logback.xml

如果,不使用logback,會直接使用 jdk 的日志實現。

使用這個java腳本

經過以上,我們的腳本文件(java:.class文件,python:.py文件,shell:.sh文件)已經准備好了,參數也准備好了(只有java需要)

創建一個 Java Scratch 文件,可以是任意類型,這里用文本文件,命名為 AutoDeploy:

這個文件沒啥用,就是為了下面綁定使用

新建一個Configuration,使用 Java Scratch 模板。
新建一個Config

填寫參數

如圖,Main Class需要填寫為上面編譯生成的 AutoDeploy。
Path to scratch file,直接選擇到剛剛新建的文件文件,這個配置沒有用處,但是不配的話,IDEA會報錯,無法執行。
VM Options 參數里,填寫第二步得到的參數,是為了 java 能正確的找到 AutoDeploy 類,並執行它的 main 方法。
Program arguments 里,可以寫自定義的參數:

host=192.168.1.166 username=bpzj password=123456 localFile="xxxx" remotepath="xxx" expect1="出現這個字符才輸入" input1="正則表達式,只獲取第一個()的匹配內容" cmd="cd /home/bpzj/ && bash ./restart.sh && tail -f xxx.log"

這里的參數會作為 AutoDeploy main方法的參數 args 傳遞過去,可以在里面解析,定義自己的邏輯。

添加運行腳本前的打包步驟

在 before launch 模塊里,添加這個配置想要打包的文件,如果是 maven 多模塊項目,也可以指定打包某個模塊。

到這里,就配置完成了,現在就可以選中這個 Configuration,點擊后,自動打包,上傳,執行我們自定義的命令。
而且,由於腳本是我們自己寫的,所以極其的靈活。

如果是 python 或 shell腳本,新建一個Configuration,使用Shell Script模塊(需要選擇對應的解釋器,如python.exe,):


TODO


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM