堡垒机服务器 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 模板。
填写参数
如图,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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)