DevOps自动化平台开发之 Shell脚本执行的封装

 基础知识

基于如下技术栈开发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();
	}

	@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);
	}

}
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() {

	}

	@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;
	}
}
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 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;
	}
}
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 脚本

猜你喜欢

转载自blog.csdn.net/Coder_Boy_/article/details/131873852