基础知识
基于如下技术栈开发DevOps平台
Spring Boot
Shell
Ansible
Git
Gitlab
Docker
K8S
Vue
1、spring boot starter的封装使用
2、Shell脚本的编写
3、Ansible 脚本的编写
4、Docker 的使用与封装设计
本篇介绍如何使用Java封装Linux命令和Shell脚本的使用
将其设计成spring boot starter
maven依赖pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.9</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.devops</groupId><artifactId>ssh-client-pool-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version><name>ssh-client-pool-spring-boot-starter</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.hierynomus</groupId><artifactId>sshj</artifactId><version>0.26.0</version></dependency><dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.60</version></dependency><dependency><groupId>com.fasterxml.uuid</groupId><artifactId>java-uuid-generator</artifactId><version>3.1.4</version></dependency><dependency><groupId>net.sf.expectit</groupId><artifactId>expectit-core</artifactId><version>0.9.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.8.0</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
具体的封装代码:
package com.devops.ssh.autoconfigure;import com.devops.ssh.pool.SshClientPoolConfig;
import com.devops.ssh.pool.SshClientsPool;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author Gary*/
@Configuration
@EnableConfigurationProperties(SshClientPoolProperties.class)
public class SshClientPoolAutoConfiguration {private final SshClientPoolProperties properties;public SshClientPoolAutoConfiguration(SshClientPoolProperties properties) {this.properties = properties;}@Bean@ConditionalOnMissingBean(SshClientsPool.class)SshClientsPool sshClientsPool() {return new SshClientsPool(sshClientPoolConfig());}SshClientPoolConfig sshClientPoolConfig() {SshClientPoolConfig poolConfig = new SshClientPoolConfig(properties.getMaxActive(),properties.getMaxIdle(),properties.getIdleTime(),properties.getMaxWait());if(properties.getSshj()!=null) {poolConfig.setServerCommandPromotRegex(properties.getSshj().getServerCommandPromotRegex());}if (properties.getSshClientImplClass()!=null) {try {poolConfig.setSshClientImplClass(Class.forName(properties.getSshClientImplClass()));} catch (ClassNotFoundException e) {e.printStackTrace();}}return poolConfig;}
}
package com.devops.ssh.autoconfigure;import org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties("devops.ssh-client-pool")
public class SshClientPoolProperties {/*** Max number of "idle" connections in the pool. Use a negative value to indicate* an unlimited number of idle connections.*/private int maxIdle = 20;/*** */private int idleTime = 120*1000;/*** Max number of connections that can be allocated by the pool at a given time.* Use a negative value for no limit.*/private int maxActive = 20;/*** Maximum amount of time (in milliseconds) a connection allocation should block* before throwing an exception when the pool is exhausted. Use a negative value* to block indefinitely.*/private int maxWait = 120*1000;private String sshClientImplClass = "com.devops.ssh.SshClientSSHJ";private SshClientProperites sshj;public int getMaxIdle() {return maxIdle;}public void setMaxIdle(int maxIdle) {this.maxIdle = maxIdle;}public int getIdleTime() {return idleTime;}public void setIdleTime(int idleTime) {this.idleTime = idleTime;}public int getMaxActive() {return maxActive;}public void setMaxActive(int maxActive) {this.maxActive = maxActive;}public int getMaxWait() {return maxWait;}public void setMaxWait(int maxWait) {this.maxWait = maxWait;}public String getSshClientImplClass() {return sshClientImplClass;}public void setSshClientImplClass(String sshClientImplClass) {this.sshClientImplClass = sshClientImplClass;}public SshClientProperites getSshj() {return sshj;}public void setSshj(SshClientProperites sshj) {this.sshj = sshj;}public static class SshClientProperites{private String serverCommandPromotRegex;public String getServerCommandPromotRegex() {return serverCommandPromotRegex;}public void setServerCommandPromotRegex(String serverCommandPromotRegex) {this.serverCommandPromotRegex = serverCommandPromotRegex;}}}
package com.devops.ssh.exception;/*** Ssh auth failed* @author Gary**/
public class AuthException extends SshException{public AuthException(String message) {this(message, null);}public AuthException(String message, Throwable error) {super(message, error);}/*** */private static final long serialVersionUID = -3961786667342327L;}
package com.devops.ssh.exception;/*** The ssh connection is disconnected* @author Gary**/
public class LostConnectionException extends SshException{private static final long serialVersionUID = -3961870786667342727L;public LostConnectionException(String message) {this(message, null);}public LostConnectionException(String message, Throwable error) {super(message, error);}
}
package com.devops.ssh.exception;public class SshException extends Exception{/*** */private static final long serialVersionUID = 2052615275027564490L;public SshException(String message, Throwable error) {super(message);if(error != null) {initCause(error);}}public SshException(String message) {this(message, null);}}
package com.devops.ssh.exception;/*** Timeout Exception* @author Gary**/
public class TimeoutException extends SshException {public TimeoutException(String message) {this(message, null);}public TimeoutException(String message, Throwable error) {super(message, error);}/****/private static final long serialVersionUID = -39618386667342727L;}
package com.devops.ssh.pool;import com.devops.ssh.SshClient;
import com.devops.ssh.SshClientSSHJ;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;/**** The configuration of SshClientPool library* <p>SshClientPoolConfig is a subclass of GenericKeyedObjectPoolConfig to control the pool behavior* <p>Also, you can replace the build-in {@link SshClient} implementation by {@link SshClientPoolConfig#setSshClientImplClass(Class)} if you want** @author Gary*/
public class SshClientPoolConfig extends GenericKeyedObjectPoolConfig<SshClientWrapper>{private Class<?> sshClientImplClass;private String serverCommandPromotRegex;public SshClientPoolConfig() {super();}/*** quick way to create SshClientPoolConfig* set TestOnBorrow to true* set TestOnReturn to true* set TestWhileIdle to true* set JmxEnabled to false* @param maxActive maxTotalPerKey* @param maxIdle maxIdlePerKey* @param idleTime idle time* @param maxWaitTime maxWaitMillis*/public SshClientPoolConfig(int maxActive, int maxIdle, long idleTime, long maxWaitTime){this.setMaxTotalPerKey(maxActive);this.setMaxIdlePerKey(maxIdle);this.setMaxWaitMillis(maxWaitTime);this.setBlockWhenExhausted(true);this.setMinEvictableIdleTimeMillis(idleTime);this.setTimeBetweenEvictionRunsMillis(idleTime);this.setTestOnBorrow(true);this.setTestOnReturn(true);this.setTestWhileIdle(true);this.setJmxEnabled(false);}public Class<?> getSshClientImplClass() {return sshClientImplClass;}/*** replace the build-in {@link SshClient} by {@link SshClientPoolConfig#setSshClientImplClass(Class)}* @param sshClientImplClass the implementation of {@link SshClient}*/public void setSshClientImplClass(Class<?> sshClientImplClass) {this.sshClientImplClass = sshClientImplClass;}/**** @return regex string used to match promot from server*/public String getServerCommandPromotRegex() {return serverCommandPromotRegex;}/*** see {@link SshClientSSHJ#setCommandPromotRegexStr(String)}* @param serverCommandPromotRegex regex string used to match promot from server*/public void setServerCommandPromotRegex(String serverCommandPromotRegex) {this.serverCommandPromotRegex = serverCommandPromotRegex;}}
package com.devops.ssh.pool;import com.devops.ssh.*;
import com.devops.ssh.exception.SshException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.UUID;/*** A wrapper class of {@link SshClient} used by {@link SshClientsPool}** @author Gary**/
public class SshClientWrapper implements SshClientEventListener {private final static Logger logger = LoggerFactory.getLogger(SshClientWrapper.class);private String id;private SshClient client;SshClientEventListener listener;SshClientConfig config;public String getId() {return this.id;}public void setListener(SshClientEventListener listener) {this.listener = listener;}public SshClientConfig getConfig() {return this.config;}public SshClientWrapper(SshClientConfig config, SshClientPoolConfig poolConfig) {this.id = UUID.randomUUID().toString();this.config = config;this.client = SshClientFactory.newInstance(config, poolConfig);}public SshClientWrapper setEventListener(SshClientEventListener listener) {this.listener = listener;this.client.setEventListener(this);return this;}public SshClientWrapper connect(int timeoutInSeconds) throws SshException {client.connect(timeoutInSeconds);return this;}public SshClientWrapper auth() throws SshException{if(null!=this.config.getPassword() && this.config.getPassword().length()>0) {client.authPassword();}else if(null!=this.config.getPrivateKeyPath() && this.config.getPrivateKeyPath().length()>0) {client.authPublickey();}else {client.authPublickey();}return this;}public SshClientWrapper startSession() throws SshException{client.startSession(true);return this;}public SshResponse executeCommand(String command, int timeoutInSeconds){SshResponse response = client.executeCommand(command, timeoutInSeconds);return response;}public void disconnect() {client.disconnect();}@Overridepublic boolean equals(Object obj) {if(obj instanceof SshClientWrapper){return id.equals(((SshClientWrapper)obj).getId());}return false;}@Overridepublic int hashCode(){return id.hashCode();}public SshClientState getState() {return client.getState();}@Overridepublic String toString() {return "["+this.id+"|"+this.config.getHost()+"|"+this.config.getPort()+"|"+this.getState()+"]";}@Overridepublic void didExecuteCommand(Object client) {this.listener.didExecuteCommand(this);}@Overridepublic void didDisConnected(Object client) {this.listener.didDisConnected(this);}@Overridepublic void didConnected(Object client) {this.listener.didConnected(this);}}
package com.devops.ssh;import com.devops.ssh.exception.SshException;/*** Ssh Client used to connect to server instance and execute command. The build-in implementation is {@link SshClientSSHJ}<p>** Client can be used in chain mode, {@link SshClient}.{@link #init(SshClientConfig)}.{@link #connect(int)}.{@link #authPassword()}.{@link #startSession(boolean)}.{@link #executeCommand(String, int)}<p>** At last, close the client with {@link #disconnect()}** <p>Set an {@link SshClientEventListener} with {@link #setEventListener(SshClientEventListener)} to be notified when its event occurs* <p>* @author Gary**/
public interface SshClient {/*** pass the {@link SshClientConfig} to client* @param config the information used to connect to server* @return SshClient itself*/public SshClient init(SshClientConfig config);/*** connect to server, and timeout if longer than {@code timeoutInSeconds}* @param timeoutInSeconds timeout in seconds* @return SshClient itself* @throws SshException if server is unreachable, usually the host and port is incorrect*/public SshClient connect(int timeoutInSeconds) throws SshException;/*** auth with password* @return SshClient itself* @throws SshException if username or password is incorrect*/public SshClient authPassword() throws SshException;/*** auth with key* @return SshClient itself* @throws SshException if username or public key is incorrect*/public SshClient authPublickey() throws SshException;/*** start session* @param shellMode <tt>true</tt>: communicate with server interactively in session, just like command line* <p><tt>false</tt>: only execute command once in session* @return SshClient itself* @throws SshException when start session failed**/public SshClient startSession(boolean shellMode) throws SshException;/**** @param command execute the {@code command} on server instance, and timeout if longer than {@code timeoutInSeconds}.* @param timeoutInSeconds timeout in seconds* @return SshResponse**/public SshResponse executeCommand(String command, int timeoutInSeconds);/*** set the listener on SshClient* @param listener notify listener when events occur in SshClient* @return SshClient itself*/public SshClient setEventListener(SshClientEventListener listener);/*** disconnect from server*/public void disconnect();/*** state of SshClient** @return SshClientState the state of ssh client* <p><tt>inited</tt> before {@link #startSession(boolean)} success* <p><tt>connected</tt> after {@link #startSession(boolean)} success* <p><tt>disconnected</tt> after {@link #disconnect()}, or any connection problem occurs*/public SshClientState getState();}
package com.devops.ssh;/**** Configuration used by {@link SshClient} to connect to remote server instance** @author Gary**/
public class SshClientConfig {private String host;private int port;private String username;private String password;private String privateKeyPath;private String id;/**** @return host address*/public String getHost() {return host;}/*** @param host host address, usually the ip address of remote server*/public void setHost(String host) {this.host = host;}/**** @return ssh port of the remote server*/public int getPort() {return port;}/*** @param port ssh port of the remote server*/public void setPort(int port) {this.port = port;}/**** @return ssh username of the remote server*/public String getUsername() {return username;}/**** @param username ssh username of the remote server*/public void setUsername(String username) {this.username = username;}/**** @return ssh password of the remote server*/public String getPassword() {return password;}/**** @param password ssh password of the remote server*/public void setPassword(String password) {this.password = password;}/*** @return ssh local key file path of the remote server*/public String getPrivateKeyPath() {return privateKeyPath;}/*** @param privateKeyPath local key file path of the remote server*/public void setPrivateKeyPath(String privateKeyPath) {this.privateKeyPath = privateKeyPath;}/**** @return id of the config*/public String getId() {return id;}/**** @param host server host address* @param port server ssh port* @param username server ssh username* @param password server ssh password* @param privateKeyPath local security key used to connect to server*/public SshClientConfig(String host, int port, String username, String password, String privateKeyPath) {this.id = host + port + username;if (null != password && password.length() > 0) {this.id += password;}if (privateKeyPath != null) {this.id += privateKeyPath;}this.host = host;this.port = port;this.username = username;this.password = password;this.privateKeyPath = privateKeyPath;}public SshClientConfig() {}@Overridepublic boolean equals(Object obj) {if (obj instanceof SshClientConfig) {return id.equals(((SshClientConfig) obj).getId());}return false;}@Overridepublic int hashCode() {return id.hashCode();}@Overridepublic String toString() {return this.id;}
}
package com.devops.ssh;/**** Set listener to a SshClient by {@link SshClient#setEventListener(SshClientEventListener)}* @author Gary**/
public interface SshClientEventListener {/*** after SshClient finished executing command* @param client the ssh client*/public void didExecuteCommand(Object client);/*** after SshClient disconnnect from the remote server* @param client the ssh client*/public void didDisConnected(Object client);/*** after SshClient start the ssh session* @param client the ssh client*/public void didConnected(Object client);
}
package com.devops.ssh;import com.devops.ssh.pool.SshClientPoolConfig;/**** Factory of {@link SshClient} implementation* <p> Create a new instance of {@link SshClientSSHJ} with {@link #newInstance(SshClientConfig)}* <p> Create a custom implementation of {@link SshClient} with {@link #newInstance(SshClientConfig, SshClientPoolConfig)}** @author Gary**/
public class SshClientFactory {/*** Create a new instance of {@link SshClientSSHJ}* @param config ssh connection configuration of the remote server* @return SshClient in inited state*/public static SshClient newInstance(SshClientConfig config){return newInstance(config, null);}/*** Create a custom implementation of {@link SshClient}* @param config ssh connection configuration of the remote server* @param poolConfig customized configuration* @return SshClient in inited state* @throws RuntimeException if SshClientImplClass in {@code poolConfig} is invalid*/public static SshClient newInstance(SshClientConfig config, SshClientPoolConfig poolConfig){try {SshClient client = null;if (poolConfig==null || poolConfig.getSshClientImplClass()==null){client = new SshClientSSHJ();}else {client = (SshClient)poolConfig.getSshClientImplClass().newInstance();}client.init(config);if(client instanceof SshClientSSHJ && poolConfig!=null && poolConfig.getServerCommandPromotRegex()!=null) {((SshClientSSHJ)client).setCommandPromotRegexStr(poolConfig.getServerCommandPromotRegex());}return client;} catch (InstantiationException e) {throw new RuntimeException("new instance failed", e);} catch (IllegalAccessException e) {throw new RuntimeException("new instance failed", e);}}}
package com.devops.ssh;import com.devops.ssh.exception.AuthException;
import com.devops.ssh.exception.LostConnectionException;
import com.devops.ssh.exception.SshException;
import com.devops.ssh.exception.TimeoutException;
import com.devops.ssh.pool.SshClientPoolConfig;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.connection.channel.direct.Session.Shell;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
import net.sf.expectit.Expect;
import net.sf.expectit.ExpectBuilder;
import net.sf.expectit.ExpectIOException;
import net.sf.expectit.Result;
import net.sf.expectit.matcher.Matcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.SocketException;
import java.nio.channels.ClosedByInterruptException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;import static net.sf.expectit.filter.Filters.removeColors;
import static net.sf.expectit.filter.Filters.removeNonPrintable;
import static net.sf.expectit.matcher.Matchers.contains;
import static net.sf.expectit.matcher.Matchers.regexp;/**** build-in {@link SshClient} implementation with <a href="https://github.com/hierynomus/sshj">hierynomus/SshJ</a>** <p>Trouble and shooting:* <p>Problem: {@link #authPublickey()} throw exceptions contains "net.schmizz.sshj.common.Buffer$BufferException:Bad item length"* <p>Solution: may caused by key file format issue,use ssh-keygen on a remote Linux server to generate the key*** @author Gary**/
public class SshClientSSHJ implements SshClient {private final static Logger logger = LoggerFactory.getLogger(SshClientSSHJ.class);private SshClientConfig clientConfig;private SSHClient client;private Expect expect = null;private Session session = null;private Shell shell = null;private boolean shellMode = false;private SshClientState state = SshClientState.inited;private SshClientEventListener eventListener;public String commandPromotRegexStr = "[\\[]?.+@.+~[\\]]?[#\\$] *";public Matcher<Result> commandPromotRegex = regexp(commandPromotRegexStr);// initialize DefaultConfig will consume resources, so we should cache itprivate static DefaultConfig defaultConfig = null;public static DefaultConfig getDefaultConfig() {if(defaultConfig==null) {defaultConfig = new DefaultConfig();}return defaultConfig;}/*** used in shell mode, once it start session with server, the server will return promot to client* <p>the promot looks like [centos@ip-172-31-31-82 ~]$* <p>if the build-in one does not fit, you can change it by {@link SshClientPoolConfig#setServerCommandPromotRegex(String)}* @param promot used to match promot from server*/public void setCommandPromotRegexStr(String promot) {this.commandPromotRegexStr = promot;this.commandPromotRegex = regexp(this.commandPromotRegexStr);}@Overridepublic SshClient init(SshClientConfig config) {this.clientConfig = config;return this;}private void validate() throws SshException {if(this.clientConfig == null) {throw new SshException("missing client config");}}@Overridepublic SshClient connect(int timeoutInSeconds) throws SshException{this.validate();if (timeoutInSeconds <= 0) {timeoutInSeconds = Integer.MAX_VALUE;} else {timeoutInSeconds = timeoutInSeconds * 1000;}return this.connect(timeoutInSeconds, false);}private SshClient connect(int timeoutInSeconds, boolean retry) throws SshException{logger.debug("connecting to " + this.clientConfig.getHost() + " port:" + this.clientConfig.getPort() + " timeout in:"+ (timeoutInSeconds / 1000) + " s");client = new SSHClient(getDefaultConfig());try {client.setConnectTimeout(timeoutInSeconds);client.addHostKeyVerifier(new PromiscuousVerifier());// client.loadKnownHosts();client.connect(this.clientConfig.getHost().trim(), this.clientConfig.getPort());logger.debug("connected to " + this.clientConfig.getHost().trim() + " port:" + this.clientConfig.getPort());} catch (TransportException e) {if(!retry) {logger.error("sshj get exception when connect and will retry one more time ", e);try {Thread.sleep(1000);} catch (InterruptedException e1) {}return this.connect(timeoutInSeconds, true);}else {String errorMessage ="connect to " + this.clientConfig.getHost().trim() + " failed";logger.error(errorMessage, e);throw new SshException(errorMessage, e);}} catch (Exception e) {String errorMessage ="connect to " + this.clientConfig.getHost().trim() + " failed";logger.error(errorMessage, e);throw new SshException(errorMessage, e);}return this;}@Overridepublic SshClient setEventListener(SshClientEventListener listener) {this.eventListener = listener;return this;}@Overridepublic SshClient authPassword() throws SshException {try {logger.debug("auth with password");client.authPassword(this.clientConfig.getUsername(), this.clientConfig.getPassword());} catch (Exception e) {String errorMessage = "ssh auth " + this.clientConfig.getHost() + " fail";logger.error(errorMessage, e);throw new AuthException(errorMessage, e);}return this;}@Overridepublic SshClient authPublickey() throws SshException {try {logger.debug("auth with key:"+this.clientConfig.getUsername()+","+this.clientConfig.getPrivateKeyPath());if (this.clientConfig.getPrivateKeyPath() != null) {KeyProvider keys = client.loadKeys(this.clientConfig.getPrivateKeyPath());client.authPublickey(this.clientConfig.getUsername(), keys);} else {client.authPublickey(this.clientConfig.getUsername());}} catch (Exception e) {String errorMessage = "ssh auth " + this.clientConfig.getHost() + " fail";logger.error(errorMessage, e);throw new AuthException(errorMessage, e);}return this;}@Overridepublic SshClient startSession(boolean shellMode) {logger.info("start session " + (shellMode ? " in shellMode" : ""));try {session = client.startSession();this.shellMode = shellMode;if (shellMode) {session.allocateDefaultPTY();shell = session.startShell();shell.changeWindowDimensions(1024, 1024, 20, 20);this.renewExpect(60);expect.expect(commandPromotRegex);}this.state = SshClientState.connected;try {if(this.eventListener!=null) {this.eventListener.didConnected(this);}} catch (Exception e) {}} catch (Exception e) {if(e instanceof ExpectIOException) {ExpectIOException ioException = (ExpectIOException)e;logger.error("start session fail with server input:"+ioException.getInputBuffer().replaceAll("[\\\n\\\r]", ""), e);}else {logger.error("start session fail", e);}this.disconnect();throw new RuntimeException("start session fail." + e.getMessage());} finally {// close expecttry {if (expect != null) {expect.close();}} catch (IOException e) {logger.error("close IO error", e);}expect = null;}return this;}@Overridepublic SshResponse executeCommand(String command, int timeoutInSeconds) {if (this.shellMode) {return this.sendCommand(command, timeoutInSeconds);} else {return this.executeCommand_(command, timeoutInSeconds);}}private SshResponse executeCommand_(String command, int timeoutInSeconds) {logger.info("execute command: " + command);SshResponse response = new SshResponse();try {Command cmd = session.exec(command);if (timeoutInSeconds < 0) {cmd.join(Long.MAX_VALUE, TimeUnit.SECONDS);} else {cmd.join(timeoutInSeconds, TimeUnit.SECONDS);}BufferedReader reader = new BufferedReader(new InputStreamReader(cmd.getInputStream(), "UTF-8"));BufferedReader error_reader = new BufferedReader(new InputStreamReader(cmd.getErrorStream(), "UTF-8"));List<String> outputLines = new ArrayList<>();logger.debug("finish executing command on " + this.clientConfig.getHost() + ", console:");String outputLine;while ((outputLine = error_reader.readLine()) != null) {logger.debug(outputLine);outputLines.add(outputLine);}while ((outputLine = reader.readLine()) != null) {logger.debug(outputLine);outputLines.add(outputLine);}response.setStdout(outputLines);logger.info("execute ssh command on " + this.clientConfig.getHost() + " completed, with exit status:" + cmd.getExitStatus());response.setCode(cmd.getExitStatus());} catch (Exception e) {if (e.getCause() instanceof InterruptedException || e.getCause() instanceof java.util.concurrent.TimeoutException) {logger.error("execute ssh on " + this.clientConfig.getHost() + " timeout");response.setException(new TimeoutException("execute ssh command timeout"));} else {logger.error("execute ssh on " + this.clientConfig.getHost() + ", command error", e);response.setException(new SshException("execute ssh command error "+e.getMessage()));}}finally {try {if(this.eventListener!=null) {this.eventListener.didExecuteCommand(this);}} catch (Exception e) {}}return response;}private SshResponse sendCommand(String command, int timeoutInSeconds) {SshResponse response = new SshResponse();if (this.state != SshClientState.connected) {response.setException(new LostConnectionException("client not connected"));response.setCode(0);return response;}try {this.renewExpect(timeoutInSeconds);// start expectlogger.info(this + " execute command : " + command);expect.send(command);logger.debug(this + " command sent ");if (!command.endsWith("\n")) {expect.send("\n");logger.debug(this + " command \\n sent ");}Result result2 = expect.expect(contains(command));Result result = expect.expect(commandPromotRegex);logger.debug("command execute success with raw output");logger.debug("------------------------------------------");String[] inputArray = result.getInput().split("\\r\\n");List<String> stout = new ArrayList<String>();if(inputArray.length>0) {for(int i=0;i<inputArray.length;i++) {logger.debug(inputArray[i]);if(i==inputArray.length-1 && inputArray[i].matches(commandPromotRegexStr)) {break;}stout.add(inputArray[i]);}}logger.debug("------------------------------------------");response.setStdout(stout);response.setCode(0);logger.info("execute ssh command on " + this.clientConfig.getHost() + " completed, with code:" + 0);} catch (Exception e) {response.setCode(1);response.setException(new SshException(e.getMessage()));logger.error("execute command fail", e);if(e instanceof ArrayIndexOutOfBoundsException) {// server may be shutdownresponse.setException(new LostConnectionException("lost connection"));this.disconnect();} else if (e instanceof ClosedByInterruptException) {response.setException(new TimeoutException("execute command timeout"));this.sendCtrlCCommand();}else if (e.getCause() instanceof SocketException) {// the socket may be closedresponse.setException(new LostConnectionException("lost connection"));this.disconnect();} else if (e.getMessage().contains("timeout")) {response.setException(new TimeoutException("execute command timeout"));this.sendCtrlCCommand();}else {this.sendCtrlCCommand();}} finally {// close expecttry {if (expect != null) {expect.close();}} catch (IOException e) {logger.error("close IO error", e);}expect = null;try {if(this.eventListener!=null) {this.eventListener.didExecuteCommand(this);}} catch (Exception e) {}}return response;}private void renewExpect(int timeoutInSeconds) throws IOException {if (expect!=null) {try {expect.close();}catch(Exception e) {e.printStackTrace();}}expect = new ExpectBuilder().withOutput(shell.getOutputStream()).withInputs(shell.getInputStream(), shell.getErrorStream()).withInputFilters(removeColors(), removeNonPrintable()).withExceptionOnFailure().withTimeout(timeoutInSeconds, TimeUnit.SECONDS).build();}private void sendCtrlCCommand() {try {logger.debug("send ctr-c command ... ");expect.send("\03");expect.expect(commandPromotRegex);logger.debug("send ctr-c command success ");} catch (IOException e1) {logger.error("send ctrl+c command fail", e1);}}@Overridepublic void disconnect() {if(this.state== SshClientState.disconnected) {return;}this.state = SshClientState.disconnected;try {if (shell != null) {shell.close();}} catch (IOException e) {logger.error("close ssh shell error", e);}try {if (session != null) {session.close();}} catch (IOException e) {logger.error("close sesion error", e);}try {if (client != null) {client.disconnect();client.close();}} catch (IOException e) {logger.error("close ssh conenction error", e);}logger.debug("ssh disconnect");try {if(this.eventListener!=null) {this.eventListener.didDisConnected(this);}} catch (Exception e) {}}@Overridepublic SshClientState getState() {return this.state;}
}
package com.devops.ssh;/**** state of SshClient, See {@link SshClient#getState()} for more information** @author Gary**/
public enum SshClientState {inited,connected,disconnected
}
package com.devops.ssh;import java.util.ArrayList;
import java.util.List;/**** Response return from {@link SshClient#executeCommand(String, int)}** @author Gary**/
public class SshResponse {private int code;private Exception exception;private List<String> stdout = new ArrayList<String>();/*** @return 0*/public int getCode() {return code;}public void setCode(int code) {this.code = code;}/**** @return the exception in {@link SshClient#executeCommand(String, int)}*/public Exception getException() {return exception;}public void setException(Exception exception) {this.exception = exception;}/**** @return the output from remote server after send command*/public List<String> getStdout() {return stdout;}public void setStdout(List<String> stdout) {this.stdout = stdout;}}
运行测试Linux命令
echo 'yes'
运行测试 shell 脚本