Przeglądaj źródła

添加 web.devops.deployer 包, 用于部署 devops 应用, 实际是代替 deployer 模块的功能

reghao 3 miesięcy temu
rodzic
commit
185c314b42

+ 49 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/controller/DeployerController.java

@@ -0,0 +1,49 @@
+package cn.reghao.bnt.web.devops.deployer.controller;
+
+import cn.reghao.bnt.web.devops.deployer.model.dto.RemoteMachine;
+import cn.reghao.bnt.web.devops.deployer.service.DeployApp;
+import cn.reghao.jutil.web.WebResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author reghao
+ * @date 2025-12-21 21:13:26
+ */
+@Slf4j
+@Tag(name = "Nginx 接口")
+@RestController
+@RequestMapping("/api/devops/deployer")
+public class DeployerController {
+    private final DeployApp deployApp;
+
+    public DeployerController(DeployApp deployApp) {
+        this.deployApp = deployApp;
+    }
+
+    @Operation(summary = "添加节点", description = "N")
+    @PostMapping(value = "/remote_host", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addRemoteHost(@RequestBody @Validated RemoteMachine remoteMachine) {
+        return WebResult.success();
+    }
+
+    @Operation(summary = "部署 devops-mgr", description = "N")
+    @PostMapping(value = "/mgr", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deployMgr(@RequestBody @Validated RemoteMachine remoteMachine) {
+        return WebResult.success();
+    }
+
+    @Operation(summary = "部署 devops-agent", description = "N")
+    @PostMapping(value = "/agent", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deployAgent(String host) {
+        deployApp.deployApp(host);
+        return WebResult.success();
+    }
+}

+ 12 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/db/RemoteHostRepository.java

@@ -0,0 +1,12 @@
+package cn.reghao.bnt.web.devops.deployer.db;
+
+import cn.reghao.bnt.web.devops.deployer.model.po.RemoteHost;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * @author reghao
+ * @date 2025-12-21 21:04:50
+ */
+public interface RemoteHostRepository extends JpaRepository<RemoteHost, Integer> {
+    RemoteHost findByHost(String host);
+}

+ 54 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/model/UserInfoImpl.java

@@ -0,0 +1,54 @@
+package cn.reghao.bnt.web.devops.deployer.model;
+
+import com.jcraft.jsch.UserInfo;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author reghao
+ * @date 2024-02-19 14:44:45
+ */
+
+/**
+ * ssh private key passphrase info
+ */
+@Slf4j
+public class UserInfoImpl implements UserInfo {
+    /**
+     * ssh private key passphrase
+     */
+    private String passphrase;
+
+    public UserInfoImpl (String passphrase) {
+        this.passphrase = passphrase;
+    }
+
+    @Override
+    public String getPassphrase() {
+        return passphrase;
+    }
+
+    @Override
+    public String getPassword() {
+        return null;
+    }
+
+    @Override
+    public boolean promptPassphrase(String s) {
+        return true;
+    }
+
+    @Override
+    public boolean promptPassword(String s) {
+        return false;
+    }
+
+    @Override
+    public boolean promptYesNo(String s) {
+        return true;
+    }
+
+    @Override
+    public void showMessage(String message) {
+        log.info ("SSH Message:{}", message);
+    }
+}

+ 13 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/model/dto/RemoteMachine.java

@@ -0,0 +1,13 @@
+package cn.reghao.bnt.web.devops.deployer.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2025-12-21 21:17:39
+ */
+@Setter
+@Getter
+public class RemoteMachine {
+}

+ 45 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/model/po/RemoteHost.java

@@ -0,0 +1,45 @@
+package cn.reghao.bnt.web.devops.deployer.model.po;
+
+import cn.reghao.bnt.web.util.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2024-02-20 09:12:10
+ */
+@NoArgsConstructor
+@Getter
+@Setter
+@Entity
+@Table(name = "devops_remote_host")
+public class RemoteHost extends BaseEntity {
+    private String host;
+    private int port;
+    private String authType;
+    private String username;
+    private String password;
+    @Column(columnDefinition="text")
+    private String privateKey;
+    private String appDir;
+
+    public RemoteHost(String host, int port, String username, String password) {
+        this.host = host;
+        this.port = port;
+        this.authType = "password";
+        this.username = username;
+        this.password = password;
+    }
+
+    public RemoteHost(String host, int port, String privateKey) {
+        this.host = host;
+        this.port = port;
+        this.authType = "privateKey";
+        this.privateKey = privateKey;
+    }
+}

+ 37 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/service/DeployApp.java

@@ -0,0 +1,37 @@
+package cn.reghao.bnt.web.devops.deployer.service;
+
+import cn.reghao.bnt.web.devops.deployer.db.RemoteHostRepository;
+import cn.reghao.bnt.web.devops.deployer.model.po.RemoteHost;
+import cn.reghao.bnt.web.devops.deployer.util.Sftp;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author reghao
+ * @date 2024-02-19 14:16:27
+ */
+@Slf4j
+@Service
+public class DeployApp {
+    private final RemoteHostRepository remoteHostRepository;
+    private Sftp sftp = new Sftp();
+
+    public DeployApp(RemoteHostRepository remoteHostRepository) {
+        this.remoteHostRepository = remoteHostRepository;
+    }
+
+    public void deployApp(String host) {
+        RemoteHost remoteHost = remoteHostRepository.findByHost(host);
+        if (remoteHost == null) {
+            return;
+        }
+
+        String localDir = "";
+        String remoteDir = remoteHost.getAppDir();
+        try {
+            sftp.deploy(localDir, remoteDir, remoteHost);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 270 - 0
web/src/main/java/cn/reghao/bnt/web/devops/deployer/util/Sftp.java

@@ -0,0 +1,270 @@
+package cn.reghao.bnt.web.devops.deployer.util;
+
+import cn.reghao.bnt.web.devops.deployer.model.po.RemoteHost;
+import cn.reghao.bnt.web.devops.deployer.model.UserInfoImpl;
+import cn.reghao.jutil.jdk.io.TextFile;
+import cn.reghao.jutil.jdk.shell.ShellResult;
+import com.jcraft.jsch.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author reghao
+ * @date 2024-02-19 11:38:54
+ */
+@Slf4j
+public class Sftp {
+    private final TextFile textFile = new TextFile();
+    private final String bash = "/usr/bin/bash";
+
+    public Session getSession(RemoteHost remoteHost) throws Exception {
+        String host = remoteHost.getHost();
+        int port = remoteHost.getPort();
+        String username = remoteHost.getUsername();
+        String password = remoteHost.getPassword();
+        JSch jsch = new JSch();
+        Session session;
+        if (port <= 0) {
+            //连接服务器,采用默认端口
+            session = jsch.getSession(username, host);
+        } else {
+            //采用指定的端口连接服务器
+            session = jsch.getSession(username, host, port);
+        }
+
+        //如果服务器连接不上,则抛出异常
+        if (session == null) {
+            throw new Exception("session is null");
+        }
+
+        if (remoteHost.getAuthType().equals("password")) {
+            //设置登陆主机的密码
+            session.setPassword(password);
+            //设置第一次登陆的时候提示,可选值:(ask | yes | no)
+            session.setConfig("StrictHostKeyChecking", "no");
+        } else if (remoteHost.getAuthType().equals("privateKey")) {
+            String privateKey = remoteHost.getPrivateKey();
+            String prikeyPath = "";
+
+            session.setConfig("PreferredAuthentications", "publickey");
+            session.setConfig("userauth.gssapi-with-mic", "no");
+            session.setConfig("StrictHostKeyChecking", "ask");
+            session.setUserInfo(new UserInfoImpl(""));
+            jsch.addIdentity(prikeyPath);
+
+            session.setConfig("UseDNS", "no");
+            session.setConfig("kex", "diffie-hellman-group1-sha1,"
+                    + "diffie-hellman-group-exchange-sha1,"
+                    + "diffie-hellman-group-exchange-sha256");
+        } else {
+            throw new Exception("password and private key not exist either");
+        }
+
+        session.connect(30_000);
+        return session;
+    }
+
+    public void upload(Session session, String local, String destDir) throws JSchException, SftpException, IOException {
+        // 创建 sftp 通信通道
+        Channel channel = session.openChannel("sftp");
+        channel.connect(5_000);
+        ChannelSftp sftp = (ChannelSftp) channel;
+        sftp.cd(destDir);
+        File localFile = new File(local);
+        if (localFile.isFile()) {
+            putFile(localFile, destDir, sftp);
+        } else {
+            for (File file : Objects.requireNonNull(localFile.listFiles())) {
+                if (file.isFile()) {
+                    putFile(file, destDir, sftp);
+                }
+            }
+        }
+
+        if (channel.isConnected()) {
+            channel.disconnect();
+        }
+    }
+
+    private void putFile(File localFile, String destDir, ChannelSftp sftp) throws IOException, SftpException {
+        String filename = localFile.getName();
+        String remoteFilePath = String.format("%s/%s", destDir, filename);
+        OutputStream outstream = sftp.put(remoteFilePath);
+
+        InputStream instream = new FileInputStream(localFile);
+        byte[] bytes = new byte[1024];
+        int n;
+        while ((n = instream.read(bytes)) != -1) {
+            outstream.write(bytes, 0, n);
+        }
+
+        outstream.flush();
+        outstream.close();
+        instream.close();
+    }
+
+    /**
+     * 在远程机器上创建目录(等价于 mkdir -p)
+     *
+     * @param
+     * @return
+     * @date 2024-02-20 09:20:11
+     */
+    public boolean mkdir(Session session, String remotePath) throws SftpException, JSchException {
+        boolean exist = true;
+        // 创建 sftp 通信通道
+        Channel channel = session.openChannel("sftp");
+        channel.connect(5_000);
+        ChannelSftp sftp = (ChannelSftp) channel;
+
+        List<String> list = new ArrayList<>();
+        String[] arr = remotePath.split("/");
+        for (int i = 0; i < arr.length; i++) {
+            String path = "/" + arr[i];
+            if (i-1 > 0) {
+                list.add(list.get(i-1) + path);
+            } else {
+                list.add(path);
+            }
+        }
+
+        for (String path : list) {
+            try {
+                sftp.stat(path);
+            } catch (SftpException e) {
+                exist = false;
+                sftp.mkdir(path);
+            }
+        }
+
+        return exist;
+    }
+
+    public ShellResult exec(Session session, String command) throws JSchException, IOException {
+        StringBuilder sb = new StringBuilder();
+        // 创建 exec 通信通道
+        ChannelExec channel = (ChannelExec) session.openChannel("exec");
+        channel.setCommand(command);
+        channel.setInputStream(null);
+        channel.setErrStream(System.err);
+        InputStream input = channel.getInputStream();
+        InputStream error = channel.getErrStream();
+
+        // timeout 设置为 10 分钟
+        channel.connect(600_000);
+        /*byte[] tmp = new byte[1024];
+        int i = 0;
+        while (true) {
+            while (input.available() > 0) {
+                i = input.read(tmp, 0, 1024);
+                if (i < 0) {
+                    break;
+                }
+            }
+
+            if (channel.isClosed()) {
+                if (input.available() > 0) {
+                    continue;
+                }
+
+                log.info("channel closed with status: {}", channel.getExitStatus());
+                break;
+            }
+
+            try {
+                Thread.sleep(1000);
+            } catch(Exception e) {
+                e.printStackTrace();
+            }
+        }
+        sb.append(new String(tmp, 0, i));
+        */
+        // stdin
+        BufferedReader inputReader = new BufferedReader(new InputStreamReader(input));
+        String line;
+        while((line = inputReader.readLine()) != null) {
+            sb.append(line).append(System.lineSeparator());
+        }
+
+        // stderr
+        StringBuilder sb1 = new StringBuilder();
+        BufferedReader errorReader = new BufferedReader(new InputStreamReader(error));
+        String line1;
+        while((line1 = errorReader.readLine()) != null) {
+            sb1.append(line1).append(System.lineSeparator());
+        }
+
+        int statusCode = channel.getExitStatus();
+        ShellResult shellResult = new ShellResult(statusCode);
+        if (statusCode == 0) {
+            shellResult.setResult(sb.toString());
+        } else {
+            shellResult.setResult(sb1.toString());
+        }
+
+        if (channel.isConnected()) {
+            channel.disconnect();
+        }
+
+        return shellResult;
+    }
+
+    public void deploy(String localDir, String remoteDir, RemoteHost remoteHost) throws Exception {
+        Session session = getSession(remoteHost);
+        boolean exist = mkdir(session, remoteDir);
+        if (exist) {
+            String command = String.format("cd %s && %s shutdown.sh", remoteDir, bash);
+            ShellResult shellResult = exec(session, command);
+            if (!shellResult.isSuccess()) {
+                log.info("shutdown application failed\nexitCode: {}\nresult:\n{}", shellResult.getExitCode(), shellResult.getResult());
+                System.exit(-1);
+            } else {
+                log.info("shutdown application successfully");
+            }
+        } else {
+            log.info("remote dir {} created", remoteDir);
+        }
+
+        upload(session, localDir, remoteDir);
+        log.info("files uploaded");
+
+        String command = String.format("cd %s && %s start.sh", remoteDir, bash);
+        ShellResult shellResult = exec(session, command);
+        if (!shellResult.isSuccess()) {
+            log.info("start application failed\nexitCode: {}\nresult:\n{}", shellResult.getExitCode(), shellResult.getResult());
+            System.exit(-1);
+        } else {
+            log.info("start application successfully");
+        }
+
+        if (session.isConnected()) {
+            session.disconnect();
+        }
+    }
+
+    public void exec(RemoteHost remoteHost, String command) throws Exception {
+        Session session = getSession(remoteHost);
+        ShellResult shellResult = exec(session, command);
+        log.info("\nexitCode: {}\nresult:\n{}", shellResult.getExitCode(), shellResult.getResult());
+
+        if (session.isConnected()) {
+            session.disconnect();
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        String host = "192.168.0.180";
+        int port = 22;
+        String username = "root";
+        String password = "gsh";
+        RemoteHost remoteHost = new RemoteHost(host, port,  username, password);
+
+        String command = "docker ps -a";
+        Sftp sftp = new Sftp();
+        sftp.exec(remoteHost, command);
+    }
+}