ssh迁移

This commit is contained in:
even 2025-05-21 19:58:46 +08:00
parent 247e65761e
commit 5e893baefa
25 changed files with 1129 additions and 235 deletions

13
dependencies/pom.xml vendored
View File

@ -89,6 +89,7 @@
<logback-classic.version>1.5.8</logback-classic.version>
<caffeine.version>2.9.3</caffeine.version>
<resilience4j-circuitbreaker.version>2.3.0</resilience4j-circuitbreaker.version>
<!-- <winrm4j.version>0.12.3</winrm4j.version>-->
</properties>
@ -650,9 +651,9 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j-spring</artifactId>
<version>${pf4j-spring.version}</version>
<groupId>io.cloudsoft.windows</groupId>
<artifactId>winrm4j</artifactId>
<version>0.12.0</version> <!-- 最新稳定版 -->
</dependency>
<!--数据库驱动-->
@ -761,6 +762,12 @@
<version>${resilience4j-circuitbreaker.version}</version>
</dependency>
<!-- devops ci- worker end-->
<!-- ssh相关依赖-->
<dependency>
<groupId>io.cloudsoft.windows</groupId>
<artifactId>winrm4j</artifactId>
<version>0.12.3</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -29,6 +29,36 @@
<groupId>cd.casic.boot</groupId>
<artifactId>spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- ssh 相关依赖,暂时先放在这-->
<dependency>
<groupId>io.cloudsoft.windows</groupId>
<artifactId>winrm4j</artifactId>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
</dependency>
<dependency>
<groupId>com.antherd</groupId>
<artifactId>sm-crypto</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>com.hierynomus</groupId>
<artifactId>sshj</artifactId>
<version>0.32.0</version>
</dependency>
<dependency>
<groupId>net.sf.expectit</groupId>
<artifactId>expectit-core</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>io.cloudsoft.windows</groupId>
<artifactId>winrm4j</artifactId>
<version>0.12.0</version> <!-- 最新稳定版 -->
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,13 @@
package cd.casic.ci.process.constant;
/**
* 命令常量
* @author cww
* @date 2022/6/17
*/
public class CommandConstant {
public static final String ENTER = "\r\n";
public static final String EXIT = "exit" + "\r\n";
public static final String SOURCE = "source-";
public static final String TARGET = "target-";
}

View File

@ -1,220 +0,0 @@
package cd.casic.ci.process.constant;
public class PipelineFinalConstant {
/**
* 项目名称
*/
public static final String appName = "arbess";
/**
* DEFAULT
*/
public static final String DEFAULT = "default";
/**
* 流水线文件系统
*/
public static final String MATFLOW_WORKSPACE = "/source";
public static final String MATFLOW_LOGS = "/artifact";
/**
* 流水线运行状态
*/
//流水线运行状态
public static final String RUN_SUCCESS = "success";
public static final String RUN_ERROR = "error";
public static final String RUN_WAIT = "wait";
public static final String RUN_HALT = "halt";
public static final String RUN_RUN = "run";
public static final String RUN_SUSPEND = "suspend";
/**
* 系统编码
*/
//字节编码
public static final String UTF_8 = "UTF-8";
public static final String GBK = "GBK";
/**
* 消息
*/
//消息发送类型
public static final String MES_PIPELINE_RUN = "PIPELINE_RUN";
//消息发送方式
public static final String MES_SEND_SITE = "site";
public static final String MES_SEND_EMAIL = "email";
public static final String MES_SEND_DINGDING = "dingding";
public static final String MES_SEND_WECHAT = "qywechat";
public static final String MES_SEND_SMS = "sms";
//消息通知方案
public static final String MES_UPDATE = "MF_MES_TYPE_UPDATE";
public static final String MES_DELETE = "MF_MES_TYPE_DELETE";
public static final String MES_CREATE = "MF_MES_TYPE_CREATE";
public static final String MES_RUN = "MF_MES_TYPE_RUN";
// 日志类型
public static final String LOG_TYPE_CREATE = "MF_LOG_TYPE_CREATE";
public static final String LOG_TYPE_DELETE = "MF_LOG_TYPE_DELETE";
public static final String LOG_TYPE_UPDATE = "MF_LOG_TYPE_UPDATE";
public static final String LOG_TYPE_RUN = "MF_LOG_TYPE_RUN";
public static final String CREATE_LINK = "/pipeline/${pipelineId}/config";
public static final String DELETE_LINK = "/pipeline/${pipelineId}/delete";
public static final String UPDATE_LINK = "/pipeline/${pipelineId}/set/info";
public static final String RUN_LINK = "/pipeline/${pipelineId}/history/${instanceId}";
/**
* 构建产物信息
*/
// 默认制品地址
public static final String PROJECT_DEFAULT_ADDRESS = "${PROJECT_DEFAULT_ADDRESS}";
public static final String DEFAULT_ARTIFACT_ADDRESS = "DEFAULT_ARTIFACT_ADDRESS";
// 默认制品
public static final String DEFAULT_ARTIFACT_NAME = "DEFAULT_ARTIFACT_NAME";
// Docker制品
public static final String DEFAULT_ARTIFACT_DOCKER = "DEFAULT_ARTIFACT_DOCKER";
// Docker名称
public static final String DEFAULT_ARTIFACT_DOCKER_NAME = "DEFAULT_ARTIFACT_DOCKER_NAME";
// 默认源码位置
public static final String DEFAULT_CODE_ADDRESS = "${DEFAULT_CODE_ADDRESS}";
public static final String DEFAULT_TYPE = "string";
/**
* 默认命令
*/
public static final String TEST_DEFAULT_ORDER = "mvn test";
public static final String MAVEN_DEFAULT_ORDER = "mvn clean package";
public static final String NODE_DEFAULT_ORDER = "npm install";
public static final String DOCKER_DEFAULT_ORDER = "docker image build -t default .";
/**
* 文件信息
*/
public static final String FILE_TEMP_PREFIX = "temp";
public static final String FILE_TYPE_TXT = ".txt";
public static final String FILE_TYPE_SH = ".sh";
public static final String FILE_TYPE_BAT = ".bat";
/**
* 系统任务类型
*/
// 源码应用类型
public static final String TASK_TYPE_CODE = "code";
public static final String TASK_CODE_GIT = "git";
public static final String TASK_CODE_GITLAB = "gitlab";
public static final String TASK_CODE_GITHUB = "github";
public static final String TASK_CODE_GITEE = "gitee";
public static final String TASK_CODE_SVN = "svn";
public static final String TASK_CODE_XCODE = "gitpuk";
public static final String TASK_CODE_DEFAULT_BRANCH = "master";
// 构建应用类型
public static final String TASK_TYPE_BUILD = "build";
public static final String TASK_BUILD_MAVEN = "maven";
public static final String TASK_BUILD_NODEJS = "nodejs";
public static final String TASK_BUILD_DOCKER = "build_docker";
// 测试应用类型
public static final String TASK_TYPE_TEST = "test";
public static final String TASK_TEST_MAVENTEST = "maventest";
public static final String TASK_TEST_TESTON = "testhubo";
// 部署应用类型
public static final String TASK_TYPE_DEPLOY = "deploy";
public static final String TASK_DEPLOY_LINUX = "liunx";
public static final String TASK_DEPLOY_DOCKER = "docker";
public static final String TASK_DEPLOY_K8S = "k8s";
// 推送制品应用类型
public static final String TASK_TYPE_ARTIFACT = "artifact";
public static final String TASK_ARTIFACT_MAVEN = "artifact_maven";
public static final String TASK_ARTIFACT_NODEJS = "artifact_nodejs";
public static final String TASK_ARTIFACT_DOCKER = "artifact_docker";
// 制品拉取应用类型
public static final String TASK_TYPE_PULL = "pull";
public static final String TASK_PULL_MAVEN = "pull_maven";
public static final String TASK_PULL_NODEJS = "pull_nodejs";
public static final String TASK_PULL_DOCKER = "pull_docker";
// 制品推送应用方式
public static final String TASK_ARTIFACT_XPACK = "hadess";
public static final String TASK_ARTIFACT_SSH = "ssh";
public static final String TASK_ARTIFACT_NEXUS = "nexus";
// 代码扫描应用类型
public static final String TASK_TYPE_CODESCAN = "codescan";
public static final String TASK_CODESCAN_SONAR = "sonar";
public static final String TASK_CODESCAN_SPOTBUGS = "spotbugs";
// 消息应用类型
public static final String TASK_TYPE_MESSAGE = "message";
public static final String TASK_MESSAGE_MSG = "message";
// 脚本应用类型
public static final String TASK_TYPE_SCRIPT = "script";
public static final String TASK_SCRIPT_SHELL = "shell";
public static final String TASK_SCRIPT_BAT = "bat";
//触发器
public static final String TRIGGER_SCHEDULED = "scheduled";
public static final String SIZE_TYPE_MB = "MB";
public static final int DEFAULT_SIZE = 2;
public static final String SIZE_TYPE_GB = "GB";
public static final Integer DEFAULT_CLEAN_CACHE_DAY = 7;
}

View File

@ -0,0 +1,13 @@
package cd.casic.ci.process.engine.constant;
public class DIYImageExecuteCommandConstant {
/**
* 机器ID
*/
public static final String MACHINE_ID ="machineId";
/**
* 人工卡点命令脚本
*/
public static final String COMMAND_SCRIPT ="commandScript";
public static final String STATUS_CODE = "statusCode";
}

View File

@ -1,6 +1,11 @@
package cd.casic.ci.process.engine.manager;
import cd.casic.ci.process.engine.message.TaskRunMessage;
import cd.casic.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import org.springframework.context.ApplicationContextAware;
/**
* 负责监听队列找到ContextManager获取runContext然后实际执行
* */
public interface WorkerManager {
public abstract class WorkerManager extends AbstractRedisStreamMessageListener<TaskRunMessage> implements ApplicationContextAware {
}

View File

@ -1,9 +1,15 @@
package cd.casic.ci.process.engine.manager.impl;
import cd.casic.ci.common.pipeline.annotation.Plugin;
import cd.casic.ci.process.engine.enums.ContextStateEnum;
import cd.casic.ci.process.engine.manager.RunContextManager;
import cd.casic.ci.process.engine.manager.WorkerManager;
import cd.casic.ci.process.engine.message.TaskRunMessage;
import cd.casic.ci.process.engine.runContext.BaseRunContext;
import cd.casic.ci.process.engine.worker.BaseWorker;
import cd.casic.ci.process.process.dataObject.task.PipTask;
import cd.casic.framework.commons.exception.ServiceException;
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
import cd.casic.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
@ -27,13 +33,15 @@ import java.util.Set;
@Component
@Slf4j
public class DefaultWorkerManager extends AbstractRedisStreamMessageListener<TaskRunMessage> implements ApplicationContextAware {
public class DefaultWorkerManager extends WorkerManager {
private static final String basePackage = "cd.casic.ci.process.engine.worker";
private Set<BeanDefinition> candidates;
private ApplicationContext applicationContext;
private Map<String,BaseWorker> taskTypeWorkerMap = null;
@Resource
private ThreadPoolTaskExecutor workerExecutor;
@Resource
private RunContextManager contextManager;
private void setTaskTypeWorker(Map<String, BaseWorker> taskTypeWorker) {
this.taskTypeWorkerMap = taskTypeWorker;
@ -70,13 +78,22 @@ public class DefaultWorkerManager extends AbstractRedisStreamMessageListener<Tas
log.info("===============接收到消息================");
try {
PipTask task = message.getTask();
BaseRunContext context = contextManager.getContext(task.getId());
if (context==null) {
throw new ServiceException(GlobalErrorCodeConstants.PIPELINE_ERROR.getCode(),"未找到对应上下文");
}
ContextStateEnum byCode = ContextStateEnum.getByCode(context.getState().get());
if (ContextStateEnum.SUSPEND.equals(byCode)) {
throw new ServiceException(GlobalErrorCodeConstants.PIPELINE_ERROR.getCode(),"当前task为挂起状态进入队列重新执行");
}
String taskType = task.getTaskType();
BaseWorker baseWorker = taskTypeWorkerMap.get(taskType);
baseWorker.setContextKey(task.getId());
workerExecutor.execute(baseWorker);
}catch (Exception e){
// TODO 后期可以考虑专门整一个队列
log.error("=======================workerManager",e);
// 这一块报错应该只可能是因为,找不到对应worker 线程池的拒绝策略 抛出错误等待redis重新消费
throw new ServiceException(GlobalErrorCodeConstants.PIPELINE_ERROR.getCode(),"");
}
}

View File

@ -1,32 +1,44 @@
package cd.casic.ci.process.engine.worker;
import cd.casic.ci.common.pipeline.annotation.Plugin;
import cd.casic.ci.process.constant.CommandConstant;
import cd.casic.ci.process.engine.enums.ContextStateEnum;
import cd.casic.ci.process.engine.manager.RunContextManager;
import cd.casic.ci.process.engine.runContext.BaseRunContext;
import cd.casic.ci.process.engine.runContext.PipelineRunContext;
import cd.casic.ci.process.engine.runContext.TaskRunContext;
import cd.casic.framework.commons.exception.ServiceException;
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
import jakarta.annotation.PostConstruct;
import cd.casic.ci.process.enums.MachineSystemEnum;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.ci.process.process.service.machine.MachineInfoService;
import cd.casic.ci.process.ssh.SshClient;
import cd.casic.ci.process.ssh.SshClientFactory;
import cd.casic.ci.process.ssh.WinRMHelper;
import jakarta.annotation.Resource;
import jodd.util.StringUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Arrays;
import java.util.List;
@Data
@Slf4j
public abstract class BaseWorker implements Runnable{
// 一些属性
@Resource
private RunContextManager contextManager;
private String contextKey;
@Resource
public MachineInfoService machineInfoService;
@Override
public void run() {
if (StringUtils.isEmpty(contextKey)) {
throw new ServiceException(GlobalErrorCodeConstants.PIPELINE_ERROR.getCode(),"请设置当前worker的contextKey");
log.error("请设置当前worker的contextKey");
return;
}
doWorker(contextKey);
}

View File

@ -0,0 +1,75 @@
package cd.casic.ci.process.engine.worker;
import cd.casic.ci.common.pipeline.annotation.Plugin;
import cd.casic.ci.process.engine.constant.DIYImageExecuteCommandConstant;
import cd.casic.ci.process.engine.runContext.TaskRunContext;
import cd.casic.ci.process.process.dataObject.base.PipBaseElement;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.ci.process.process.dataObject.task.PipTask;
import cd.casic.framework.commons.exception.ServiceException;
import cd.casic.framework.commons.exception.enums.GlobalErrorCodeConstants;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义镜像执行命令
*
* @author herenbin
* @date 2022-11-08 9:59
*/
@Slf4j
@Plugin(taskType = "CUSTOM_IMAGE_EXECUTION_COMMAND")
public class DIYImageExecuteCommandWorker extends SshWorker {
@Override
public void execute(TaskRunContext context) {
int statusCode = -1;
AtomicInteger state = context.getState();
if (context.getContextDef() instanceof PipTask taskDef) {
log.info(taskDef.getTaskName());
Map<String, Object> taskProperties = taskDef.getTaskProperties();
Object commandScriptObj = taskProperties.get(DIYImageExecuteCommandConstant.COMMAND_SCRIPT);
Object machineIdObj = taskProperties.get(DIYImageExecuteCommandConstant.MACHINE_ID);
String commandScript = commandScriptObj instanceof String ? ((String) commandScriptObj) : null;
Long machineId = null;
try {
machineId=Long.valueOf(String.valueOf(machineIdObj));
} catch (NumberFormatException e) {
log.error("缺少参数:{}",DIYImageExecuteCommandConstant.MACHINE_ID);
}
if (StringUtils.isEmpty(commandScript) ||machineIdObj == null) {
throw new ServiceException(GlobalErrorCodeConstants.PIPELINE_ERROR.getCode(),"缺少参数");
}
try {
//将节点的配置信息反编译成对象
log.info("构建脚本" + commandScript);
//如果machineId为0则说明该节点没有配置机器则使用开始节点的机器
//获取机器
MachineInfo machineInfoDO = this.getMachineInfoService().getById(machineId);
statusCode = shell(machineInfoDO,
"echo \"自定义镜像执行命令\"",
commandScript
);
} catch (Exception e) {
String errorMessage = "该节点配置信息为空,请先配置该节点信息" + "\r\n";
// errorHandle(e, errorMessage);
}
if (statusCode == 0) {
// log.info("{}节点执行完成", getName());
} else {
// log.error("{}节点执行失败", getName());
}
context.getLocalVariables().put(DIYImageExecuteCommandConstant.STATUS_CODE,statusCode);
}
}
}

View File

@ -0,0 +1,81 @@
package cd.casic.ci.process.engine.worker;
import cd.casic.ci.process.constant.CommandConstant;
import cd.casic.ci.process.enums.MachineSystemEnum;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.ci.process.ssh.SshClient;
import cd.casic.ci.process.ssh.SshClientFactory;
import cd.casic.ci.process.ssh.WinRMHelper;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
@Slf4j
public abstract class SshWorker extends BaseWorker{
/**
* 执行shell命令
* @param machineInfo 机器
* @param commands 命令
* @return 0 成功其他值 失败
*/
public int shell(MachineInfo machineInfo, String... commands) {
List<String> commandList = Arrays.asList(commands);
if(MachineSystemEnum.WINDOWS.getSystem().equals(machineInfo.getOsSystem())){
return powerShell(machineInfo, commandList);
}
// NodeLogger nodeLogger = nodeLoggerThreadLocal.get();
int statusCode = -1;
// String loggerUuid = nodeLogger.getLoggerUuid();
// String nodeUuid = nodeLogger.getNodeUuid();
//loggerUuid得转换成String类型才能生成key然后才能通过websocket实时推送节点执行日志
SshClient ssh = null;
try {
ssh = SshClientFactory.createSsh(machineInfo);
//执行命令并且把命令的执行回传到前端
statusCode = ssh.execNew(commandList, var -> {
// TODO 记录日志
// loggerService.sendMessage(key, var);
// nodeLogger.append(var);
});
log.info("exit-status: " + statusCode);
//主动释放当前socket连接
// loggerService.close(key);
} catch (Exception e) {
String errorMessage = "与机器建立SSH连接出错" + CommandConstant.ENTER;
// errorHandle(e, errorMessage);
} finally {
if(ssh!=null) {
ssh.disconnect();
}
}
return statusCode;
}
/**
* 执行shell命令
* @param machineInfo 机器
* @param commandList 命令
* @return 0 成功其他值 失败
*/
public int powerShell(MachineInfo machineInfo, List<String> commandList) {
int statusCode = -1;
//loggerUuid得转换成String类型才能生成key然后才能通过websocket实时推送节点执行日志
try {
WinRMHelper winRmHelper = new WinRMHelper(machineInfo);
statusCode = winRmHelper.executePs(commandList, var ->{
log.info("================================================================" + var);
// TODO 记录日志
// loggerService.sendMessage(key, var);
// nodeLogger.append(var);
});
log.info("exit-status: " + statusCode);
//主动释放当前socket连接
} catch (Exception e) {
String errorMessage = "与Window机器建立winrm连接出错" + CommandConstant.ENTER;
// errorHandle(e, errorMessage);
}
return statusCode;
}
}

View File

@ -0,0 +1,27 @@
package cd.casic.ci.process.enums;
/**
* 枚举
* @author herenbin
* @date 2023/6/25
*/
public enum MachineSystemEnum {
/**
* Linux
*/
LINUX("Linux"),
/**
* Windows
*/
WINDOWS("Windows");
private final String system;
MachineSystemEnum(String system) {
this.system = system;
}
public String getSystem() {
return system;
}
}

View File

@ -0,0 +1,15 @@
package cd.casic.ci.process.process.dal.machine;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.framework.mybatis.core.mapper.BaseMapperX;
/**
* 机器信息表
*
* @author herenbin
* @date 2022-09-27 10:10:37
*/
public interface MachineInfoDao extends BaseMapperX<MachineInfo> {
}

View File

@ -4,7 +4,9 @@ import cd.casic.framework.commons.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class PipBaseElement extends BaseDO {
/**

View File

@ -0,0 +1,84 @@
package cd.casic.ci.process.process.dataObject.machine;
import cd.casic.framework.commons.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 机器信息表
*
* @author herenbin
* @date 2022-09-27 10:10:37
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("machine_info")
public class MachineInfo extends BaseDO {
/**
* 机器认证方式 1: 账号认证 2: key认证
*/
private String authType;
/**
* 机器描述
*/
private String description;
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 主机ip
*/
private String machineHost;
/**
* 机器名称
*/
private String machineName;
/**
* 机器状态 1有效 2无效
*/
private String machineStatus;
/**
* 机器唯一标识
*/
private String machineTag;
/**
* 机器密码
*/
private String password;
/**
* 代理id
*/
private Long proxyId;
/**
* ssh端口
*/
private Integer sshPort;
/**
* 机器账号
*/
private String username;
/**
* 机器账号
*/
@TableField("os_system")
private String osSystem;
}

View File

@ -0,0 +1,17 @@
package cd.casic.ci.process.process.service.machine;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* 机器信息表service接口
*
* @author herenbin
* @date 2022-09-27 10:25:29
*/
public interface MachineInfoService extends IService<MachineInfo> {
}

View File

@ -0,0 +1,25 @@
package cd.casic.ci.process.process.service.machine.impl;
import cd.casic.ci.process.process.dal.machine.MachineInfoDao;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.ci.process.process.service.machine.MachineInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 机器信息表service接口实现类
* @author herenbin
* @date 2022-09-27 10:25:29
*/
@Slf4j
@Service
public class MachineInfoServiceImpl extends ServiceImpl<MachineInfoDao, MachineInfo> implements MachineInfoService {
}

View File

@ -0,0 +1,19 @@
package cd.casic.ci.process.ssh;
/**
* @ClassName ops
* @Descriptions ssh 命令回调
* @Author mianbin
* @Date 2022/6/22 15:26
* @Version 1.0
**/
@FunctionalInterface
public interface ExecCallback {
/**
* 回调执行方法
* @param out 命令执行的输出
*/
void callback(String out);
}

View File

@ -0,0 +1,56 @@
package cd.casic.ci.process.ssh;
import java.io.IOException;
import java.util.List;
/**
* @ClassName ops
* @Description ssh 命令 , 不记录
* @Author mianbin
* @Date 2022/6/22 15:27
* @Version 1.0
**/
public interface SshClient {
/**
* 执行ssh 命令
*
* @param command 要执行的命令多个命令通过";"号隔开
* @param execCallback 命令执行过程中的回调器
* @return 执行成功返回0失败返回1
* @throws IOException
*/
int exec(String command, ExecCallback execCallback) throws IOException;
/**
* 执行ssh 命令
*
* @param commands 要执行的命令多个命令通过";"号隔开
* @param execCallback 命令执行过程中的回调器
* @return 执行成功返回0失败返回1
* @throws IOException
*/
int execNew(List<String> commands, ExecCallback execCallback) throws IOException;
/**
* 执行ssh 命令
*
* @param commands 要执行的命令多个命令通过";"号隔开
* @param execCallback 命令执行过程中的回调器
* @return 执行成功返回0失败返回1
* @throws IOException
*/
int parallelDebugExec(List<String> commands, ExecCallback execCallback) throws IOException;
/**
* 执行多行命令
*
* @param command
* @param execCallback
* @return
* @throws IOException
*/
int exec(List<String> command, ExecCallback execCallback) throws IOException;
/**
* 断开客户端连接
*/
void disconnect();
}

View File

@ -0,0 +1,45 @@
package cd.casic.ci.process.ssh;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import com.jcraft.jsch.JSchException;
/**
* @ClassName ops
* @Description ssh 工厂 ,这里主要使用的是这个,做个封装
* @Author mianbin
* @Date 2022/6/22 15:40
* @Version 1.0
**/
public class SshClientFactory {
/**
* 基于用户名密码认证创建客户端
* @param username 用户名
* @param password 密码
* @param host 主机ip
* @param port ssh端口号
* @return
*/
public static SshClient createSsh(String username, String password, String host, int port) throws Exception {
try {
return new SshCommand(username, password, host, port);
} catch (JSchException e) {
throw new Exception("创建ssh客户端出错", e);
}
}
/**
*
* 用户名密码或私钥连接客户端
* @param properties 配置参数
* 主要包含 用户名 密码 私钥 主机ip 端口 认证类型
*
* */
public static SshClient createSsh(MachineInfo properties) throws Exception {
try {
return new SshCommand(properties);
} catch (JSchException e) {
throw new Exception("创建ssh客户端出错", e);
}
}
}

View File

@ -0,0 +1,253 @@
package cd.casic.ci.process.ssh;
import cd.casic.ci.process.constant.CommandConstant;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.ci.process.util.ChannelShellUtil;
import cd.casic.ci.process.util.CryptogramUtil;
import cn.hutool.extra.ssh.JschUtil;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import java.io.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @ClassName ops
* @Description ssh 的命令操作的集合 ,这里是做个统一,其他pipleline 和web ternminal 的实现方式不同
* @Author mianbin
* @Date 2022/6/22 15:43
* @Version 1.0
**/
public class SshCommand implements SshClient {
private static final int BUFF_SIZE = 1024 * 8; // 8KB
/**
* jsch session
*/
Session session;
/**
* 基于用户名密码认证的构造器
*
* @param username 用户名
* @param password 密码
* @param host 主机ip
* @param port ssh端口号
* @throws JSchException
*/
public SshCommand(String username, String password, String host, int port) throws JSchException {
this.session = JschUtil.createSession(host, port, username, password);
this.session.setConfig("PreferredAuthentications", "password");
this.session.setConfig("StrictHostKeyChecking", "no");
this.session.setPassword(password);
this.session.connect();
}
/**
* 基于用户名密码认证的构造器
* Machine实体类
*
* @throws JSchException
*/
public SshCommand(MachineInfo machine) throws JSchException {
// 根据用户名主机ip端口获取一个Session对象
String decrypt = CryptogramUtil.doDecrypt(machine.getPassword());
this.session = JschUtil.createSession(machine.getMachineHost(), machine.getSshPort(), machine.getUsername(), decrypt);
this.session.setConfig("PreferredAuthentications", "password");
this.session.setConfig("StrictHostKeyChecking", "no");
// 通过Session建立链接
this.session.connect();
}
@Override
public void disconnect() {
if(session!=null) {
session.disconnect();
}
}
@Override
public int exec(String command, ExecCallback execCallback) throws IOException {
ChannelShell channel = null;
try {
channel = (ChannelShell) session.openChannel("shell");
channel.connect();
} catch (JSchException e) {
e.printStackTrace();
}
InputStream inputStream = channel.getInputStream();
OutputStream outputStream = channel.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(command);
printWriter.println("exit");
printWriter.flush();
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
String msg = null;
while ((msg = in.readLine()) != null) {
execCallback.callback(msg);
}
int result = channel.getExitStatus();
in.close();
return result;
}
@Override
public int exec(List<String> commands, ExecCallback execCallback) throws IOException {
ChannelShell channel = null;
try {
channel = (ChannelShell) session.openChannel("shell");
channel.connect();
} catch (JSchException e) {
e.printStackTrace();
}
assert channel != null;
ChannelShellUtil.setDefault(channel);
InputStream inputStream = channel.getInputStream();
OutputStream outputStream = channel.getOutputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr, BUFF_SIZE);
PrintStream commander = new PrintStream(outputStream, true);
for (String command : commands) {
commander.append(command).append(CommandConstant.ENTER);
}
commander.append("exit" + CommandConstant.ENTER);
try {
char[] buff = new char[BUFF_SIZE];
int read;
while ((read = br.read(buff)) != -1) {
String output = String.valueOf(buff, 0, read);
execCallback.callback( output + CommandConstant.ENTER);
TimeUnit.MILLISECONDS.sleep(10L);
// 检测输出如果满足跳出条件则退出循环
if (output.contains("# exit")||output.contains("$ exit")) {
commander.append("exit" + CommandConstant.ENTER);
TimeUnit.MILLISECONDS.sleep(1000L);
break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
inputStream.close();
outputStream.close();
channel.disconnect();
int result = channel.getExitStatus();
commander.close();
isr.close();
br.close();
return result;
}
@Override
public int execNew(List<String> commands, ExecCallback execCallback) throws IOException {
ChannelShell channel = null;
try {
channel = (ChannelShell) session.openChannel("shell");
channel.connect();
} catch (JSchException e) {
e.printStackTrace();
}
assert channel != null;
ChannelShellUtil.setDefault(channel);
InputStream inputStream = channel.getInputStream();
OutputStream outputStream = channel.getOutputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr, BUFF_SIZE);
PrintStream commander = new PrintStream(outputStream, true);
for (String command : commands) {
commander.append(command).append(CommandConstant.ENTER);
}
commander.append("exit" + CommandConstant.ENTER);
try {
char[] buff = new char[BUFF_SIZE];
int read;
while ((read = br.read(buff)) != -1) {
execCallback.callback(String.valueOf(buff, 0, read) + CommandConstant.ENTER);
TimeUnit.MILLISECONDS.sleep(10L);
}
} catch (Exception ex) {
ex.printStackTrace();
}
inputStream.close();
outputStream.close();
channel.disconnect();
int result = channel.getExitStatus();
commander.close();
isr.close();
br.close();
return result;
}
@Override
public int parallelDebugExec(List<String> commands, ExecCallback execCallback) throws IOException {
ChannelShell channel = null;
try {
channel = (ChannelShell) session.openChannel("shell");
channel.connect();
} catch (JSchException e) {
e.printStackTrace();
}
assert channel != null;
ChannelShellUtil.setDefault(channel);
InputStream inputStream = channel.getInputStream();
OutputStream outputStream = channel.getOutputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
BufferedReader br = new BufferedReader(isr, BUFF_SIZE);
PrintStream commander = new PrintStream(outputStream, true);
for (String command : commands) {
commander.append(command).append(CommandConstant.ENTER);
}
try {
char[] buff = new char[BUFF_SIZE];
int read;
while ((read = br.read(buff)) != -1) {
String msg = String.valueOf(buff, 0, read);
execCallback.callback(msg + CommandConstant.ENTER);
TimeUnit.MILLISECONDS.sleep(20L);
if(msg.endsWith("$ ") || msg.endsWith("# ")) {
commander.append("exit").append(CommandConstant.ENTER);
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
int result = channel.getExitStatus();
inputStream.close();
outputStream.close();
channel.disconnect();
commander.close();
isr.close();
br.close();
return result;
}
}

View File

@ -0,0 +1,168 @@
package cd.casic.ci.process.ssh;
import cd.casic.ci.process.process.dataObject.machine.MachineInfo;
import cd.casic.ci.process.util.CryptogramUtil;
import io.cloudsoft.winrm4j.client.WinRmClientContext;
import io.cloudsoft.winrm4j.winrm.WinRmTool;
import io.cloudsoft.winrm4j.winrm.WinRmToolResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.util.List;
/**
* @author herenbin
* @date 2023/6/26
*/
@Slf4j
public class WinRMHelper {
private final String ip;
private final String username;
private final String password;
public static final int DEFAULT_PORT = WinRmTool.DEFAULT_WINRM_PORT;
public WinRMHelper(final String ip, final String username, final String password) {
this.ip = ip;
this.username = username;
this.password = password;
}
public WinRMHelper(MachineInfo machineInfo) {
this.ip = machineInfo.getMachineHost();
this.username = machineInfo.getUsername();
this.password = CryptogramUtil.doDecrypt(machineInfo.getPassword());
}
public int execute(final String command, ExecCallback execCallback) {
WinRmClientContext context = WinRmClientContext.newInstance();
try {
WinRmTool tool = WinRmTool.Builder.builder(ip, username, password)
.port(DEFAULT_PORT).useHttps(false).context(context).build();
tool.setOperationTimeout(5000L);
WinRmToolResponse resp = tool.executeCommand(command);
int statusCode = resp.getStatusCode();
execCallback.callback(resp.getStdOut());
String stdErr = resp.getStdErr();
if (StringUtils.isNotEmpty(stdErr)) {
execCallback.callback(stdErr);
}
return statusCode;
} finally {
context.shutdown();
}
}
public int execute(final List<String> commandList, ExecCallback execCallback) {
WinRmClientContext context = WinRmClientContext.newInstance();
WinRmTool tool = WinRmTool.Builder.builder(ip, username, password)
.port(DEFAULT_PORT).useHttps(false).context(context).build();
tool.setOperationTimeout(5000L);
WinRmToolResponse resp = tool.executeCommand(commandList);
StringReader reader = new StringReader(resp.getStdOut() + resp.getStdErr());
try {
char[] buffer = new char[1024];
int output;
while ((output = reader.read(buffer)) != -1) {
execCallback.callback(new String(buffer, 0, output));
}
} catch (Exception e) {
e.printStackTrace();
}
int statusCode = resp.getStatusCode();
context.shutdown();
return statusCode;
}
public int executePs(final List<String> commandList, ExecCallback execCallback) {
WinRmClientContext context = WinRmClientContext.newInstance();
WinRmTool tool = WinRmTool.Builder.builder(ip, username, password)
.port(DEFAULT_PORT).useHttps(false).context(context).build();
tool.setOperationTimeout(5000L);
WinRmToolResponse resp = tool.executePs(commandList);
StringReader reader = new StringReader(resp.getStdOut() + resp.getStdErr());
try {
char[] buffer = new char[1024];
int output;
while ((output = reader.read(buffer)) != -1) {
execCallback.callback(new String(buffer, 0, output));
}
} catch (Exception e) {
e.printStackTrace();
}
int statusCode = resp.getStatusCode();
context.shutdown();
return statusCode;
}
public String execute(final List<String> commandList, ExecCallback execCallback, String pass) {
WinRmClientContext context = WinRmClientContext.newInstance();
WinRmTool tool = WinRmTool.Builder.builder(ip, username, password)
.port(DEFAULT_PORT).useHttps(false).context(context).build();
tool.setOperationTimeout(5000L);
WinRmToolResponse resp = tool.executeCommand(commandList);
context.shutdown();
return resp.getStdOut();
}
private String join(List<String> commands) {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String command : commands) {
if (first) {
first = false;
} else {
builder.append(" & ");
}
builder.append(command);
}
return builder.toString();
}
public static void main(String[] args) throws IOException {
// WinRMHelper exec = new WinRMHelper("192.168.0.88", "hrb", "1qaz!QAZ");
// List<String> commands = new ArrayList<>();
//// commands.add("powershell");
// commands.add("dir");
// commands.add("cd /d D:\\Users\\hrb\\Desktop");
// commands.add("dir");
// commands.add("git clone -b master http://192.168.0.12:3000/liuyuchao/testrepo.git");
//// commands.add("rmdir /s /q 456123");
//
// int resp = exec.execute(commands, System.out::println);
// System.out.println(resp);
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader(writer);
Thread t = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
writer.write("Line " + i + "\n");
writer.flush();
Thread.sleep(500);
}
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
});
t.start();
try (BufferedReader br = new BufferedReader(reader)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,14 @@
package cd.casic.ci.process.util;
import com.jcraft.jsch.ChannelShell;
public class ChannelShellUtil {
public static void setDefault(ChannelShell channel) {
// channel.setEnv("LANG", "zh_CN.UTF-8");
channel.setEnv("LANG", "en_US.UTF-8");
// SSH 代理转发
channel.setAgentForwarding(false);
channel.setPtyType("xterm");
}
}

View File

@ -0,0 +1,135 @@
package cd.casic.ci.process.util;
import cn.hutool.log.Log;
import com.antherd.smcrypto.sm2.Sm2;
import com.antherd.smcrypto.sm3.Sm3;
import com.antherd.smcrypto.sm4.Sm4;
import com.antherd.smcrypto.sm4.Sm4Options;
/**
* 加密工具类本框架目前使用 https://github.com/antherd/sm-crypto 项目中一些加解密方式
* 使用小伙伴需要过等保密评相关请在此处更改为自己的加密方法或加密机使用加密机同时需要替换公钥私钥在内部无法导出提供加密的方法
*
* @author yubaoshan
*/
public class CryptogramUtil {
private static final Log log = Log.get();
/**
* 加密方法Sm2 的专门针对前后端分离非对称秘钥对的方式暴露出去的公钥对传输过程中的密码加个密
*
* @param str 待加密数据
* @return 加密后的密文
* @author yubaoshan
*/
public static String doSm2Encrypt(String str) {
return Sm2.doEncrypt(str, Keypair.PUBLIC_KEY);
}
/**
* 解密方法
* 如果采用加密机的方法用try catch 捕捉异常返回原文值即可
*
* @param str 密文
* @return 解密后的明文
* @author yubaoshan
*/
public static String doSm2Decrypt(String str) {
// 解密
return Sm2.doDecrypt(str, Keypair.PRIVATE_KEY);
}
/**
* 加密方法
*
* @param str 待加密数据
* @return 加密后的密文
* @author yubaoshan
*/
public static String doEncrypt(String str) {
// SM4 加密 cbc模式
Sm4Options sm4Options4 = new Sm4Options();
sm4Options4.setMode("cbc");
sm4Options4.setIv("fedcba98765432100123456789abcdef");
return Sm4.encrypt(str, Keypair.KEY, sm4Options4);
}
/**
* 解密方法
* 如果采用加密机的方法用try catch 捕捉异常返回原文值即可
*
* @param str 密文
* @return 解密后的明文
* @author yubaoshan
*/
public static String doDecrypt(String str) {
// 解密cbc 模式输出 utf8 字符串
Sm4Options sm4Options8 = new Sm4Options();
sm4Options8.setMode("cbc");
sm4Options8.setIv("fedcba98765432100123456789abcdef");
String docString = Sm4.decrypt(str, Keypair.KEY, sm4Options8);
if (docString.equals("")) {
log.warn(">>> 字段解密失败,返回原文值:{}", str);
return str;
} else {
return docString;
}
}
/**
* 纯签名
*
* @param str 待签名数据
* @return 签名结果
* @author yubaoshan
*/
public static String doSignature(String str) {
return Sm2.doSignature(str, Keypair.PRIVATE_KEY);
}
/**
* 验证签名结果
*
* @param originalStr 签名原文数据
* @param str 签名结果
* @return 是否通过
* @author yubaoshan
*/
public static boolean doVerifySignature(String originalStr, String str) {
return Sm2.doVerifySignature(originalStr, str, Keypair.PUBLIC_KEY);
}
/**
* 通过杂凑算法取得hash值用于做数据完整性保护
*
* @param str 字符串
* @return hash
* @author yubaoshan
*/
public static String doHashValue(String str) {
return Sm3.sm3(str);
}
private static class Keypair{
/**
* 公钥
*/
public static String PUBLIC_KEY = "04298364ec840088475eae92a591e01284d1abefcda348b47eb324bb521bb03b0b2a5bc393f6b71dabb8f15c99a0050818b56b23f31743b93df9cf8948f15ddb54";
/**
* 私钥
*/
public static String PRIVATE_KEY = "3037723d47292171677ec8bd7dc9af696c7472bc5f251b2cec07e65fdef22e25";
/**
* SM4的对称秘钥生产环境需要改成自己使用的
* 16 进制字符串要求为 128 比特
*/
public static String KEY = "0123456789abcdeffedcba9876543210";
}
}

View File

@ -14,7 +14,7 @@ public interface DictDataConvert {
DictDataConvert INSTANCE = Mappers.getMapper(DictDataConvert.class);
List<DictDataSimpleRespVO> convertList(List<DictDataDO> list);
// List<DictDataSimpleRespVO> convertList(List<DictDataDO> list);
DictDataRespVO convert(DictDataDO bean);

View File

@ -27,5 +27,6 @@ public interface DictTypeConvert {
// List<DictTypeExcelVO> convertList02(List<DictTypeDO> list);
List<DictDataTreeVO> convertList03(List<DictTypeDO> list);
DictDataTreeVO converter03(DictTypeDO typeDO);
}