堡垒机服务器 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

posted @   bpzj  阅读(2601)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示