前段時間接了一個比較特殊的需求,須要作一個用於部署服務的服務。主要是將一個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
這裏介紹下幾個主要的工具方法運維
先定義一個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操做官方給了很詳細的示例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.
複製代碼
能夠看到字母已經都是大寫了
上面這些方法,基本上覆蓋了咱們平常在服務器上進行操做的場景了,那麼無論部署服務,仍是運維服務器都不成問題了