基础知识
基于如下技术栈开发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>
具体的封装代码:
java
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;
}
}
java
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;
}
}
}
java
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;
}
java
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);
}
}
java
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);
}
}
java
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;
}
java
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;
}
}
java
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();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof SshClientWrapper){
return id.equals(((SshClientWrapper)obj).getId());
}
return false;
}
@Override
public int hashCode(){
return id.hashCode();
}
public SshClientState getState() {
return client.getState();
}
@Override
public String toString() {
return "["+this.id+"|"
+this.config.getHost()+"|"
+this.config.getPort()+"|"
+this.getState()+"]";
}
@Override
public void didExecuteCommand(Object client) {
this.listener.didExecuteCommand(this);
}
@Override
public void didDisConnected(Object client) {
this.listener.didDisConnected(this);
}
@Override
public void didConnected(Object client) {
this.listener.didConnected(this);
}
}
java
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();
}
java
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() {
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SshClientConfig) {
return id.equals(((SshClientConfig) obj).getId());
}
return false;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return this.id;
}
}
java
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);
}
java
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);
}
}
}
java
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 it
private 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);
}
@Override
public SshClient init(SshClientConfig config) {
this.clientConfig = config;
return this;
}
private void validate() throws SshException {
if(this.clientConfig == null) {
throw new SshException("missing client config");
}
}
@Override
public 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;
}
@Override
public SshClient setEventListener(SshClientEventListener listener) {
this.eventListener = listener;
return this;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public 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 expect
try {
if (expect != null) {
expect.close();
}
} catch (IOException e) {
logger.error("close IO error", e);
}
expect = null;
}
return this;
}
@Override
public 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 expect
logger.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 shutdown
response.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 closed
response.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 expect
try {
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);
}
}
@Override
public 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) {
}
}
@Override
public SshClientState getState() {
return this.state;
}
}
java
package com.devops.ssh;
/**
*
* state of SshClient, See {@link SshClient#getState()} for more information
*
* @author Gary
*
*/
public enum SshClientState {
inited,
connected,
disconnected
}
java
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 脚本