JSch-用java實現服務器遠程操做

介紹

前段時間接了一個比較特殊的需求,須要作一個用於部署服務的服務。主要是將一個k8s服務集羣部署到遠端的服務器上,具體服務器的鏈接信息會經過接口傳入。html

原本部署是人工來完成的,無非是將一些必須的文件scp到目標服務器上,而後ssh遠程登陸,執行一些安裝的操做,齊活。安裝的流程沒什麼問題,主要是這些步驟須要使用代碼來實現,也就是須要一個支持SSH的client庫來執行這些操做java

最終選用了JSch(Java Secure Channel),官網是這麼介紹的:服務器

JSch is a pure Java implementation of SSH2.
JSch allows you to connect to an sshd server and use port forwarding, X11 forwarding, file transfer, etc., and you can integrate its functionality into your own Java programs. JSch is licensed under BSD style license.session

實現

爲了完成部署服務的任務,須要解決幾個問題:app

  • SSH鏈接到遠端的服務器
  • 在服務器上執行指令
  • 使用scp命令傳輸文件
  • 編輯服務器上的文件,主要是爲了修改一些配置文件

這裏介紹下幾個主要的工具方法運維

遠程ssh鏈接

先定義一個Remote類,用於記錄服務器登陸信息ssh

@Data
public class Remote {

    private String user = "root";
    private String host = "127.0.0.1";
    private int port = 22;
    private String password = "";
    private String identity = "~/.ssh/id_rsa";
    private String passphrase = "";
}
複製代碼

這裏填充了一些默認值,平時用的時候方便一些ide

JSch使用Session來定義一個遠程節點:工具

public static Session getSession(Remote remote) throws JSchException {
    JSch jSch = new JSch();
    if (Files.exists(Paths.get(remote.getIdentity()))) {
        jSch.addIdentity(remote.getIdentity(), remote.getPassphrase());
    }
    Session session = jSch.getSession(remote.getUser(), remote.getHost(),remote.getPort());
    session.setPassword(remote.getPassword());
    session.setConfig("StrictHostKeyChecking", "no");
    return session;
}
複製代碼

測試一下:測試

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        System.out.println("Host({}) connected.", remote.getHost);
    }
    session.disconnect();
}
複製代碼

正確的輸入了服務器地址和密碼後,鏈接成功。

這裏要提一下,JSch會優先使用填入的ssh_key去嘗試登陸,嘗試失敗後纔會使用password登陸,這點和平時使用ssh命令的交互是一致的,好評~

遠程指令

接下來就是編寫一個通用的方法,用於在Session上執行命令

public static List<String> remoteExecute(Session session, String command) throws JSchException {
    log.debug(">> {}", command);
    List<String> resultLines = new ArrayList<>();
    ChannelExec channel = null;
    try{
        channel = (ChannelExec) session.openChannel("exec");
        channel.setCommand(command);
        InputStream input = channel.getInputStream();
        channel.connect(CONNECT_TIMEOUT);
        try {
            BufferedReader inputReader = new BufferedReader(newInputStreamReader(input));
            String inputLine = null;
            while((inputLine = inputReader.readLine()) != null) {
                log.debug(" {}", inputLine);
                resultLines.add(inputLine);
            }
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (Exception e) {
                    log.error("JSch inputStream close error:", e);
                }
            }
        }
    } catch (IOException e) {
        log.error("IOcxecption:", e);
    } finally {
        if (channel != null) {
            try {
                channel.disconnect();
            } catch (Exception e) {
                log.error("JSch channel disconnect error:", e);
            }
        }
    }
    return resultLines;
}
複製代碼

測試一下:

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        System.out.println("Host({}) connected.", remote.getHost());
    }
    
    remoteExecute(session, "pwd");
    remoteExecute(session, "mkdir /root/jsch-demo");
    remoteExecute(session, "ls /root/jsch-demo");
    remoteExecute(session, "touch /root/jsch-demo/test1; touch /root/jsch-demo/test2");
    remoteExecute(session, "echo 'It a test file.' > /root/jsch-demo/test-file");
    remoteExecute(session, "ls -all /root/jsch-demo");
    remoteExecute(session, "ls -all /root/jsch-demo | grep test");
    remoteExecute(session, "cat /root/jsch-demo/test-file");
    
    session.disconnect();
}
複製代碼

執行後,日誌輸出以下內容:

Host(192.168.124.20) connected.
>> pwd
   /root
>> mkdir /root/jsch-demo
>> ls /root/jsch-demo
>> touch /root/jsch-demo/test1; touch /root/jsch-demo/test2
>> echo 'It a test file.' > /root/jsch-demo/test-file
>> ls -all /root/jsch-demo
   total 12
   drwxr-xr-x 2 root root 4096 Jul 30 03:05 .
   drwx------ 6 root root 4096 Jul 30 03:05 ..
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test1
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test2
   -rw-r--r-- 1 root root   16 Jul 30 03:05 test-file
>> ls -all /root/jsch-demo | grep test
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test1
   -rw-r--r-- 1 root root    0 Jul 30 03:05 test2
   -rw-r--r-- 1 root root   16 Jul 30 03:05 test-file
>> cat /root/jsch-demo/test-file
   It a test file.
複製代碼

執行結果使人滿意,這些常見的命令都成功了
再次好評~

scp操做

scp操做官方給了很詳細的示例scpTo+scpFrom,再次好評~
scpTo:

public static long scpTo(String source, Session session, String destination) {
    FileInputStream fileInputStream = null;
    try {
        ChannelExec channel = (ChannelExec) session.openChannel("exec");
        OutputStream out = channel.getOutputStream();
        InputStream in = channel.getInputStream();
        boolean ptimestamp = false;
        String command = "scp";
        if (ptimestamp) {
            command += " -p";
        }
        command += " -t " + destination;
        channel.setCommand(command);
        channel.connect(CONNECT_TIMEOUT);
        if (checkAck(in) != 0) {
            return -1;
        }
        File _lfile = new File(source);
        if (ptimestamp) {
            command = "T " + (_lfile.lastModified() / 1000) + " 0";
            // The access time should be sent here,
            // but it is not accessible with JavaAPI ;-<
            command += (" " + (_lfile.lastModified() / 1000) + " 0\n");
            out.write(command.getBytes());
            out.flush();
            if (checkAck(in) != 0) {
                return -1;
            }
        }
        //send "C0644 filesize filename", where filename should not include '/'
        long fileSize = _lfile.length();
        command = "C0644 " + fileSize + " ";
        if (source.lastIndexOf('/') > 0) {
            command += source.substring(source.lastIndexOf('/') + 1);
        } else {
            command += source;
        }
        command += "\n";
        out.write(command.getBytes());
        out.flush();
        if (checkAck(in) != 0) {
            return -1;
        }
        //send content of file
        fileInputStream = new FileInputStream(source);
        byte[] buf = new byte[1024];
        long sum = 0;
        while (true) {
            int len = fileInputStream.read(buf, 0, buf.length);
            if (len <= 0) {
                break;
            }
            out.write(buf, 0, len);
            sum += len;
        }
        //send '\0'
        buf[0] = 0;
        out.write(buf, 0, 1);
        out.flush();
        if (checkAck(in) != 0) {
            return -1;
        }
        return sum;
    } catch(JSchException e) {
        log.error("scp to catched jsch exception, ", e);
    } catch(IOException e) {
        log.error("scp to catched io exception, ", e);
    } catch(Exception e) {
        log.error("scp to error, ", e);
    } finally {
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (Exception e) {
                log.error("File input stream close error, ", e);
            }
        }
    }
    return -1;
}
複製代碼

scpFrom:

public static long scpFrom(Session session, String source, String destination) {
    FileOutputStream fileOutputStream = null;
    try {
        ChannelExec channel = (ChannelExec) session.openChannel("exec");
        channel.setCommand("scp -f " + source);
        OutputStream out = channel.getOutputStream();
        InputStream in = channel.getInputStream();
        channel.connect();
        byte[] buf = new byte[1024];
        //send '\0'
        buf[0] = 0;
        out.write(buf, 0, 1);
        out.flush();
        while(true) {
            if (checkAck(in) != 'C') {
                break;
            }
        }
        //read '644 '
        in.read(buf, 0, 4);
        long fileSize = 0;
        while (true) {
            if (in.read(buf, 0, 1) < 0) {
                break;
            }
            if (buf[0] == ' ') {
                break;
            }
            fileSize = fileSize * 10L + (long)(buf[0] - '0');
        }
        String file = null;
        for (int i = 0; ; i++) {
            in.read(buf, i, 1);
            if (buf[i] == (byte) 0x0a) {
                file = new String(buf, 0, i);
                break;
            }
        }
        // send '\0'
        buf[0] = 0;
        out.write(buf, 0, 1);
        out.flush();
        // read a content of lfile
        if (Files.isDirectory(Paths.get(destination))) {
            fileOutputStream = new FileOutputStream(destination + File.separator +file);
        } else {
            fileOutputStream = new FileOutputStream(destination);
        }
        long sum = 0;
        while (true) {
            int len = in.read(buf, 0 , buf.length);
            if (len <= 0) {
                break;
            }
            sum += len;
            if (len >= fileSize) {
                fileOutputStream.write(buf, 0, (int)fileSize);
                break;
            }
            fileOutputStream.write(buf, 0, len);
            fileSize -= len;
        }
        return sum;
    } catch(JSchException e) {
        log.error("scp to catched jsch exception, ", e);
    } catch(IOException e) {
        log.error("scp to catched io exception, ", e);
    } catch(Exception e) {
        log.error("scp to error, ", e);
    } finally {
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (Exception e) {
                log.error("File output stream close error, ", e);
            }
        }
    }
    return -1;
}
複製代碼

另外還有一個公用的方法checkAck:

private static int checkAck(InputStream in) throws IOException {
    int b=in.read();
    // b may be 0 for success,
    // 1 for error,
    // 2 for fatal error,
    // -1
    if(b==0) return b;
    if(b==-1) return b;
    if(b==1 || b==2){
        StringBuffer sb=new StringBuffer();
        int c;
        do {
            c=in.read();
            sb.append((char)c);
        }
        while(c!='\n');
        if(b==1){ // error
            log.debug(sb.toString());
        }
        if(b==2){ // fatal error
            log.debug(sb.toString());
        }
    }
    return b;
}
複製代碼

測試一下:
咱們在項目根目錄下新建一個文件test.txt

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        log.debug("Host({}) connected.", remote.getHost());
    }
    
    remoteExecute(session, "ls /root/jsch-demo/");
    scpTo("test.txt", session, "/root/jsch-demo/");
    remoteExecute(session, "ls /root/jsch-demo/");
    remoteExecute(session, "echo ' append text.' >> /root/jsch-demo/test.txt");
    scpFrom(session, "/root/jsch-demo/test.txt", "file-from-remote.txt");
    
    session.disconnect();
}
複製代碼

日誌輸出以下: 並且能夠看到項目目錄下出現了一個文件file-from-remote.txt。裏面的內容比原先的test.txt多了 append text

Host(192.168.124.20) connected.
>> ls /root/jsch-demo/
   test1
   test2
   test-file
>> ls /root/jsch-demo/
   test1
   test2
   test-file
   test.txt
>> echo ' append text.' >> /root/jsch-demo/test.txt
複製代碼

遠程編輯

咱們平時在服務器上編輯文件通常使用vi,很是方便,可是在這裏操做vi就有點複雜了
最後採用的方案是,先將源文件備份,而後scp拉到本地,編輯完後scp回原位置
remoteEdit方法:

private static boolean remoteEdit(Session session, String source, Function<List<String>, List<String>> process) {
    InputStream in = null;
    OutputStream out = null;
    try {
        String fileName = source;
        int index = source.lastIndexOf('/');
        if (index >= 0) {
            fileName = source.substring(index + 1);
        }
        //backup source
        remoteExecute(session, String.format("cp %s %s", source, source + ".bak." +System.currentTimeMillis()));
        //scp from remote
        String tmpSource = System.getProperty("java.io.tmpdir") + session.getHost() +"-" + fileName;
        scpFrom(session, source, tmpSource);
        in = new FileInputStream(tmpSource);
        //edit file according function process
        String tmpDestination = tmpSource + ".des";
        out = new FileOutputStream(tmpDestination);
        List<String> inputLines = new ArrayList<>();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String inputLine = null;
        while ((inputLine = reader.readLine()) != null) {
            inputLines.add(inputLine);
        }
        List<String> outputLines = process.apply(inputLines);
        for (String outputLine : outputLines) {
            out.write((outputLine + "\n").getBytes());
            out.flush();
        }
        //scp to remote
        scpTo(tmpDestination, session, source);
        return true;
    } catch (Exception e) {
        log.error("remote edit error, ", e);
        return false;
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (Exception e) {
                log.error("input stream close error", e);
            }
        }
        if (out != null) {
            try {
                out.close();
            } catch (Exception e) {
                log.error("output stream close error", e);
            }
        }
    }
}
複製代碼

測試一下:

public static void main(String[] args) throws Exception {
    Remote remote = new Remote();
    remote.setHost("192.168.124.20");
    remote.setPassword("123456");
    Session session = getSession(remote);
    session.connect(CONNECT_TIMEOUT);
    if (session.isConnected()) {
        log.debug("Host({}) connected.", remote.getHost());
    }
    
    remoteExecute(session, "echo 'It a test file.' > /root/jsch-demo/test");
    remoteExecute(session, "cat /root/jsch-demo/test");
    remoteEdit(session, "/root/jsch-demo/test", (inputLines) -> {
        List<String> outputLines = new ArrayList<>();
        for (String inputLine : inputLines) {
            outputLines.add(inputLine.toUpperCase());
        }
        return outputLines;
    });
    remoteExecute(session, "cat /root/jsch-demo/test");
    
    session.disconnect();
}
複製代碼

執行後日志輸出:

Host(192.168.124.20) connected.
>> echo 'It a test file.' > /root/jsch-demo/test
>> cat /root/jsch-demo/test
   It a test file.
>> cp /root/jsch-demo/test /root/jsch-demo/test.bak.1564556060191
>> cat /root/jsch-demo/test
   IT A TEST FILE.
複製代碼

能夠看到字母已經都是大寫了

總結

上面這些方法,基本上覆蓋了咱們平常在服務器上進行操做的場景了,那麼無論部署服務,仍是運維服務器都不成問題了

相關文章
相關標籤/搜索