introduce
Some time ago, I received a special requirement to make a service to deploy the service. A K8S service cluster is deployed to a remote server, and the connection information of the specific server is passed in through the interface.
The original deployment is manual to complete, is nothing more than some necessary file SCP to the target server, and then SSH remote login, perform some installation operations, complete work. There is nothing wrong with the installation process, except that these steps need to be implemented in code, i.e. a client library that supports SSH to perform these operations
JSch(Java Secure Channel) was selected.
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.
implementation
To accomplish the task of deploying the service, several issues need to be addressed:
- SSH Connects to a remote server
- Execute instructions on the server
- Use the SCP command to transfer files
- Edit files on the server, mainly to modify some configuration files
Here are some of the main tool approaches
Remote SSH Connection
Start by defining a Remote class for logging server login information
@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 = "";
}
Copy the code
Here are some default values to make it easier to use
JSch uses sessions to define a remote node:
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;
}
Copy the code
Test it out:
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();
}
Copy the code
After entering the correct server address and password, the connection succeeds.
JSch will use the ssh_key to log in first, and then use the password if the attempt fails. This is the same as SSH
The remote command
The next step is to write a generic method for executing commands on a 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;
}
Copy the code
Test it out:
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();
}
Copy the code
After the command is executed, the following information is displayed:
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.Copy the code
The results were satisfactory, and these common commands were successful
SCP operation
ScpTo +scpFrom
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;
}
Copy the code
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;
}
Copy the code
There is also a common method 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 errorlog.debug(sb.toString()); }}return b;
}
Copy the code
To test this, create a new file called test.txt in the project root directory
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();
}
Copy the code
The log output is as follows: and you can see that a file file-from-remote. TXT appears in the project directory. It contains more append text than test. TXT
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
Copy the code
Remote editing
The remoteEdit method is used to back up the source file, then pull the SCP to the local directory, and then return the SCP to the original location.
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); }}}}Copy the code
Test it out:
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();
}
Copy the code
Log output after execution:
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.
Copy the code
You can see that the letters are already in capitals
conclusion
The above methods basically cover the scenario of our daily operation on the server, so no matter the deployment of services, or the operation and maintenance of the server are not a problem