first commit
This commit is contained in:
87
orion-ops-api/orion-ops-service/pom.xml
Normal file
87
orion-ops-api/orion-ops-service/pom.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>cn.orionsec.ops</groupId>
|
||||
<artifactId>orion-ops-api</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>orion-ops-service</name>
|
||||
<artifactId>orion-ops-service</artifactId>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<dependencies>
|
||||
<!-- common -->
|
||||
<dependency>
|
||||
<groupId>cn.orionsec.ops</groupId>
|
||||
<artifactId>orion-ops-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- model -->
|
||||
<dependency>
|
||||
<groupId>cn.orionsec.ops</groupId>
|
||||
<artifactId>orion-ops-model</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- dao -->
|
||||
<dependency>
|
||||
<groupId>cn.orionsec.ops</groupId>
|
||||
<artifactId>orion-ops-dao</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- web socket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- devtools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- tests -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ding -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
package cn.orionsec.ops.config;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource")
|
||||
public DataSource druidDataSource() {
|
||||
return new DruidDataSource();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
package cn.orionsec.ops.config;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.alibaba.fastjson.support.config.FastJsonConfig;
|
||||
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class JsonSerializerConfig {
|
||||
|
||||
@Bean
|
||||
public HttpMessageConverters fastJsonHttpMessageConverters() {
|
||||
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
|
||||
FastJsonConfig fastJsonConfig = new FastJsonConfig();
|
||||
List<MediaType> mediaTypes = Lists.newList();
|
||||
mediaTypes.add(MediaType.APPLICATION_JSON);
|
||||
fastJsonHttpMessageConverter.setSupportedMediaTypes(mediaTypes);
|
||||
fastJsonConfig.setSerializerFeatures(
|
||||
SerializerFeature.DisableCircularReferenceDetect,
|
||||
SerializerFeature.WriteMapNullValue,
|
||||
SerializerFeature.WriteNullListAsEmpty,
|
||||
SerializerFeature.IgnoreNonFieldGetter
|
||||
);
|
||||
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
|
||||
return new HttpMessageConverters(fastJsonHttpMessageConverter);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
package cn.orionsec.ops.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
@Configuration
|
||||
public class RedisSerializeConfig {
|
||||
|
||||
@Bean
|
||||
public <T> RedisTemplate<String, T> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, T> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
redisTemplate.setKeySerializer(RedisSerializer.string());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.json());
|
||||
redisTemplate.setHashKeySerializer(RedisSerializer.string());
|
||||
redisTemplate.setHashValueSerializer(RedisSerializer.json());
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
package cn.orionsec.ops.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
@EnableScheduling
|
||||
@Configuration
|
||||
public class SchedulerConfig {
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(4);
|
||||
scheduler.setRemoveOnCancelPolicy(true);
|
||||
scheduler.setThreadNamePrefix("scheduling-task-");
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
package cn.orionsec.ops.handler.alarm;
|
||||
|
||||
import cn.orionsec.ops.constant.machine.MachineAlarmType;
|
||||
import cn.orionsec.ops.entity.domain.UserInfoDO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class MachineAlarmContext {
|
||||
|
||||
/**
|
||||
* 报警机器id
|
||||
*/
|
||||
private Long machineId;
|
||||
|
||||
/**
|
||||
* 报警机器名称
|
||||
*/
|
||||
private String machineName;
|
||||
|
||||
/**
|
||||
* 报警主机
|
||||
*/
|
||||
private String machineHost;
|
||||
|
||||
/**
|
||||
* 报警类型
|
||||
*
|
||||
* @see MachineAlarmType
|
||||
*/
|
||||
private Integer alarmType;
|
||||
|
||||
/**
|
||||
* 报警值
|
||||
*/
|
||||
private Double alarmValue;
|
||||
|
||||
/**
|
||||
* 报警时间
|
||||
*/
|
||||
private Date alarmTime;
|
||||
|
||||
/**
|
||||
* 用户映射
|
||||
*/
|
||||
private Map<Long, UserInfoDO> userMapping;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
package cn.orionsec.ops.handler.alarm;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.alarm.AlarmGroupNotifyType;
|
||||
import cn.orionsec.ops.dao.UserInfoDAO;
|
||||
import cn.orionsec.ops.entity.domain.AlarmGroupNotifyDO;
|
||||
import cn.orionsec.ops.entity.domain.AlarmGroupUserDO;
|
||||
import cn.orionsec.ops.entity.domain.MachineAlarmGroupDO;
|
||||
import cn.orionsec.ops.entity.domain.UserInfoDO;
|
||||
import cn.orionsec.ops.handler.alarm.push.AlarmWebSideMessagePusher;
|
||||
import cn.orionsec.ops.handler.alarm.push.AlarmWebhookPusher;
|
||||
import cn.orionsec.ops.service.api.AlarmGroupNotifyService;
|
||||
import cn.orionsec.ops.service.api.AlarmGroupUserService;
|
||||
import cn.orionsec.ops.service.api.MachineAlarmGroupService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class MachineAlarmExecutor implements Runnable, Executable {
|
||||
|
||||
private static final MachineAlarmGroupService machineAlarmGroupService = SpringHolder.getBean(MachineAlarmGroupService.class);
|
||||
|
||||
private static final AlarmGroupUserService alarmGroupUserService = SpringHolder.getBean(AlarmGroupUserService.class);
|
||||
|
||||
private static final AlarmGroupNotifyService alarmGroupNotifyService = SpringHolder.getBean(AlarmGroupNotifyService.class);
|
||||
|
||||
private static final UserInfoDAO userInfoDAO = SpringHolder.getBean(UserInfoDAO.class);
|
||||
|
||||
private final MachineAlarmContext context;
|
||||
|
||||
public MachineAlarmExecutor(MachineAlarmContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
Threads.start(this, SchedulerPools.MACHINE_ALARM_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("机器触发报警推送 context: {}", JSON.toJSONString(context));
|
||||
// 查询报警组
|
||||
List<Long> alarmGroupIdList = machineAlarmGroupService.selectByMachineId(context.getMachineId())
|
||||
.stream()
|
||||
.map(MachineAlarmGroupDO::getGroupId)
|
||||
.collect(Collectors.toList());
|
||||
log.info("机器触发报警推送 groupId: {}", alarmGroupIdList);
|
||||
if (alarmGroupIdList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 查询报警组员
|
||||
List<Long> alarmUserIdList = alarmGroupUserService.selectByGroupIdList(alarmGroupIdList)
|
||||
.stream()
|
||||
.map(AlarmGroupUserDO::getUserId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
log.info("机器触发报警推送 userId: {}", alarmUserIdList);
|
||||
if (alarmGroupIdList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// 查询用户信息
|
||||
Map<Long, UserInfoDO> userMapping = userInfoDAO.selectBatchIds(alarmUserIdList)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(UserInfoDO::getId, Function.identity()));
|
||||
context.setUserMapping(userMapping);
|
||||
// 通知站内信
|
||||
this.doAlarmPush(alarmGroupIdList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行报警推送
|
||||
*
|
||||
* @param alarmGroupIdList 报警组id
|
||||
*/
|
||||
private void doAlarmPush(List<Long> alarmGroupIdList) {
|
||||
// 通知站内信
|
||||
new AlarmWebSideMessagePusher(context).push();
|
||||
// 通知报警组
|
||||
List<AlarmGroupNotifyDO> alarmNotifyList = alarmGroupNotifyService.selectByGroupIdList(alarmGroupIdList);
|
||||
for (AlarmGroupNotifyDO alarmNotify : alarmNotifyList) {
|
||||
Integer notifyType = alarmNotify.getNotifyType();
|
||||
if (AlarmGroupNotifyType.WEBHOOK.getType().equals(notifyType)) {
|
||||
// 通知 webhook
|
||||
try {
|
||||
new AlarmWebhookPusher(alarmNotify.getNotifyId(), context).push();
|
||||
} catch (Exception e) {
|
||||
log.error("机器报警 webhook 推送失败 id: {}", alarmNotify.getNotifyId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
package cn.orionsec.ops.handler.alarm.push;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.math.Numbers;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.machine.MachineAlarmType;
|
||||
import cn.orionsec.ops.constant.message.MessageType;
|
||||
import cn.orionsec.ops.handler.alarm.MachineAlarmContext;
|
||||
import cn.orionsec.ops.service.api.WebSideMessageService;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class AlarmWebSideMessagePusher implements IAlarmPusher {
|
||||
|
||||
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
|
||||
|
||||
private final MachineAlarmContext context;
|
||||
|
||||
public AlarmWebSideMessagePusher(MachineAlarmContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push() {
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.NAME, context.getMachineName());
|
||||
params.put(EventKeys.HOST, context.getMachineHost());
|
||||
params.put(EventKeys.TYPE, MachineAlarmType.of(context.getAlarmType()).getLabel());
|
||||
params.put(EventKeys.VALUE, Numbers.setScale(context.getAlarmValue(), 2));
|
||||
params.put(EventKeys.TIME, Dates.format(context.getAlarmTime()));
|
||||
context.getUserMapping().forEach((k, v) -> {
|
||||
webSideMessageService.addMessage(MessageType.MACHINE_ALARM, context.getMachineId(), k, v.getUsername(), params);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
|
||||
package cn.orionsec.ops.handler.alarm.push;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.math.Numbers;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.machine.MachineAlarmType;
|
||||
import cn.orionsec.ops.constant.webhook.WebhookType;
|
||||
import cn.orionsec.ops.dao.WebhookConfigDAO;
|
||||
import cn.orionsec.ops.entity.domain.UserInfoDO;
|
||||
import cn.orionsec.ops.entity.domain.WebhookConfigDO;
|
||||
import cn.orionsec.ops.handler.alarm.MachineAlarmContext;
|
||||
import cn.orionsec.ops.handler.webhook.DingRobotPusher;
|
||||
import cn.orionsec.ops.utils.ResourceLoader;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AlarmWebhookPusher implements IAlarmPusher {
|
||||
|
||||
private static final String DING_TEMPLATE = "/templates/push/machine-alarm-ding.template";
|
||||
|
||||
private static WebhookConfigDAO webhookConfigDAO = SpringHolder.getBean(WebhookConfigDAO.class);
|
||||
|
||||
private final Long webhookId;
|
||||
|
||||
private final MachineAlarmContext context;
|
||||
|
||||
public AlarmWebhookPusher(Long webhookId, MachineAlarmContext context) {
|
||||
this.webhookId = webhookId;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push() {
|
||||
// 查询 webhook
|
||||
WebhookConfigDO webhook = webhookConfigDAO.selectById(webhookId);
|
||||
if (webhook == null) {
|
||||
return;
|
||||
}
|
||||
// 触发 webhook
|
||||
if (WebhookType.DING_ROBOT.getType().equals(webhook.getWebhookType())) {
|
||||
// 钉钉机器人
|
||||
this.doDingRobotPush(webhook);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行钉钉机器人推送
|
||||
*
|
||||
* @param webhook webhook
|
||||
*/
|
||||
private void doDingRobotPush(WebhookConfigDO webhook) {
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.NAME, context.getMachineName());
|
||||
params.put(EventKeys.HOST, context.getMachineHost());
|
||||
params.put(EventKeys.VALUE, Numbers.setScale(context.getAlarmValue(), 2));
|
||||
params.put(EventKeys.TYPE, MachineAlarmType.of(context.getAlarmType()).getLabel());
|
||||
params.put(EventKeys.TIME, Dates.format(context.getAlarmTime()));
|
||||
String text = Strings.format(ResourceLoader.get(DING_TEMPLATE, AlarmWebhookPusher.class), params);
|
||||
// @ 的用户
|
||||
List<String> atMobiles = context.getUserMapping()
|
||||
.values()
|
||||
.stream()
|
||||
.map(UserInfoDO::getContactPhone)
|
||||
.collect(Collectors.toList());
|
||||
// 推送
|
||||
DingRobotPusher.builder()
|
||||
.url(webhook.getWebhookUrl())
|
||||
.title("机器发生报警")
|
||||
.text(text)
|
||||
.atMobiles(atMobiles)
|
||||
.build()
|
||||
.push();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
package cn.orionsec.ops.handler.alarm.push;
|
||||
|
||||
public interface IAlarmPusher {
|
||||
|
||||
/**
|
||||
* 执行推送
|
||||
*/
|
||||
void push();
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.Letters;
|
||||
import cn.orionsec.kit.lang.define.io.OutputAppender;
|
||||
import cn.orionsec.kit.lang.exception.ExecuteException;
|
||||
import cn.orionsec.kit.lang.exception.LogException;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.ssh.ExitCode;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.app.ActionStatus;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.dao.ApplicationActionLogDAO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractActionHandler implements IActionHandler {
|
||||
|
||||
protected static ApplicationActionLogDAO applicationActionLogDAO = SpringHolder.getBean(ApplicationActionLogDAO.class);
|
||||
|
||||
protected Long id;
|
||||
|
||||
protected Long relId;
|
||||
|
||||
protected MachineActionStore store;
|
||||
|
||||
protected ApplicationActionLogDO action;
|
||||
|
||||
protected OutputStream outputSteam;
|
||||
|
||||
protected OutputAppender appender;
|
||||
|
||||
protected volatile boolean terminated;
|
||||
|
||||
protected Date startTime, endTime;
|
||||
|
||||
@Getter
|
||||
protected volatile ActionStatus status;
|
||||
|
||||
public AbstractActionHandler(Long id, MachineActionStore store) {
|
||||
this.id = id;
|
||||
this.relId = store.getRelId();
|
||||
this.store = store;
|
||||
this.action = store.getActions().get(id);
|
||||
this.status = ActionStatus.of(action.getRunStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
log.info("应用操作执行-开始: relId: {}, id: {}", relId, id);
|
||||
// 状态检查
|
||||
if (!ActionStatus.WAIT.equals(status)) {
|
||||
return;
|
||||
}
|
||||
Exception ex = null;
|
||||
// 执行
|
||||
try {
|
||||
// 更新状态
|
||||
this.updateStatus(ActionStatus.RUNNABLE);
|
||||
// 打开日志
|
||||
this.openLogger();
|
||||
// 执行
|
||||
this.handler();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 成功回调
|
||||
this.successCallback();
|
||||
} else {
|
||||
// 异常回调
|
||||
this.exceptionCallback(ex);
|
||||
throw Exceptions.runtime(ex.getMessage(), ex);
|
||||
}
|
||||
} finally {
|
||||
// 释放资源
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流程
|
||||
*
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
protected abstract void handler() throws Exception;
|
||||
|
||||
@Override
|
||||
public void skip() {
|
||||
log.info("应用操作执行-跳过: relId: {}, id: {}", relId, id);
|
||||
if (ActionStatus.WAIT.equals(status)) {
|
||||
// 只能跳过等待中的任务
|
||||
this.updateStatus(ActionStatus.SKIPPED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
log.info("应用操作执行-终止: relId: {}, id: {}", relId, id);
|
||||
this.terminated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开日志
|
||||
*/
|
||||
protected void openLogger() {
|
||||
String logPath = Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), action.getLogPath());
|
||||
log.info("应用操作执行-打开日志 relId: {}, id: {}, path: {}", relId, id, logPath);
|
||||
File logFile = new File(logPath);
|
||||
Files1.touch(logFile);
|
||||
this.outputSteam = Files1.openOutputStreamFastSafe(logFile);
|
||||
this.appender = OutputAppender.create(outputSteam).then(store.getSuperLogStream());
|
||||
// 拼接开始日志
|
||||
this.appendStartedLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接开始日志
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void appendStartedLog() {
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(StainCode.prefix(StainCode.GLOSS_GREEN))
|
||||
.append("# ").append(action.getActionName()).append(" 执行开始")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
ActionType actionType = ActionType.of(action.getActionType());
|
||||
if (ActionType.BUILD_COMMAND.equals(actionType) || ActionType.RELEASE_COMMAND.equals(actionType)) {
|
||||
log.append(Letters.LF)
|
||||
.append(Utils.getStainKeyWords("# 执行命令", StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF)
|
||||
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
|
||||
.append(Utils.getEndLfWithEof(action.getActionCommand()))
|
||||
.append(StainCode.SUFFIX);
|
||||
}
|
||||
store.getSuperLogStream().write(Strings.bytes(Const.LF_3));
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
private void terminatedCallback() {
|
||||
log.info("应用操作执行-终止回调: relId: {}, id: {}", relId, id);
|
||||
// 修改状态
|
||||
this.updateStatus(ActionStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF)
|
||||
.append(StainCode.prefix(StainCode.GLOSS_YELLOW))
|
||||
.append("# ").append(action.getActionName()).append(" 手动停止")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功回调
|
||||
*/
|
||||
private void successCallback() {
|
||||
log.info("应用操作执行-成功回调: relId: {}, id: {}", relId, id);
|
||||
// 修改状态
|
||||
this.updateStatus(ActionStatus.FINISH);
|
||||
// 拼接完成日志
|
||||
this.appendFinishedLog(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*
|
||||
* @param ex ex
|
||||
*/
|
||||
private void exceptionCallback(Exception ex) {
|
||||
log.error("应用操作执行-异常回调: relId: {}, id: {}", relId, id, ex);
|
||||
// 修改状态
|
||||
this.updateStatus(ActionStatus.FAILURE);
|
||||
// 拼接完成日志
|
||||
this.appendFinishedLog(ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接完成日志
|
||||
*
|
||||
* @param ex ex
|
||||
*/
|
||||
private void appendFinishedLog(Exception ex) {
|
||||
StringBuilder log = new StringBuilder();
|
||||
Integer actionType = action.getActionType();
|
||||
if (ActionType.BUILD_COMMAND.getType().equals(actionType) || ActionType.RELEASE_COMMAND.getType().equals(actionType)) {
|
||||
log.append(Const.LF);
|
||||
}
|
||||
if (ex != null) {
|
||||
// 有异常
|
||||
log.append(StainCode.prefix(StainCode.GLOSS_RED))
|
||||
.append("# ").append(action.getActionName()).append(" 执行失败")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE));
|
||||
Integer exitCode = this.getExitCode();
|
||||
if (exitCode != null) {
|
||||
log.append(" exitcode: ")
|
||||
.append(ExitCode.isSuccess(exitCode)
|
||||
? Utils.getStainKeyWords(exitCode, StainCode.GLOSS_BLUE)
|
||||
: Utils.getStainKeyWords(exitCode, StainCode.GLOSS_RED));
|
||||
}
|
||||
log.append(Letters.LF);
|
||||
} else {
|
||||
// 无异常
|
||||
long used = endTime.getTime() - startTime.getTime();
|
||||
log.append(StainCode.prefix(StainCode.GLOSS_GREEN))
|
||||
.append("# ").append(action.getActionName()).append(" 执行完成")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(" used ")
|
||||
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
|
||||
.append(" (")
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append(used)
|
||||
.append("ms")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(")\n");
|
||||
}
|
||||
// 拼接异常
|
||||
if (ex != null && !(ex instanceof ExecuteException)) {
|
||||
log.append(Const.LF);
|
||||
if (ex instanceof LogException) {
|
||||
log.append(Utils.getStainKeyWords(ex.getMessage(), StainCode.GLOSS_RED));
|
||||
} else {
|
||||
log.append(Exceptions.getStackTraceAsString(ex));
|
||||
}
|
||||
log.append(Const.LF);
|
||||
}
|
||||
// 拼接日志
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接日志
|
||||
*
|
||||
* @param log log
|
||||
*/
|
||||
@SneakyThrows
|
||||
protected void appendLog(String log) {
|
||||
appender.write(Strings.bytes(log));
|
||||
appender.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
protected void updateStatus(ActionStatus status) {
|
||||
Date now = new Date();
|
||||
this.status = status;
|
||||
ApplicationActionLogDO update = new ApplicationActionLogDO();
|
||||
update.setId(id);
|
||||
update.setRunStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
this.startTime = now;
|
||||
update.setStartTime(now);
|
||||
break;
|
||||
case FINISH:
|
||||
case FAILURE:
|
||||
case TERMINATED:
|
||||
this.endTime = now;
|
||||
update.setEndTime(now);
|
||||
update.setExitCode(this.getExitCode());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// 更新状态
|
||||
applicationActionLogDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 关闭日志
|
||||
Streams.close(outputSteam);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationRepositoryDO;
|
||||
import cn.orionsec.ops.service.api.ApplicationRepositoryService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import org.eclipse.jgit.api.CloneCommand;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.ResetCommand;
|
||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class CheckoutActionHandler extends AbstractActionHandler {
|
||||
|
||||
private static final ApplicationRepositoryService applicationRepositoryService = SpringHolder.getBean(ApplicationRepositoryService.class);
|
||||
|
||||
private Git git;
|
||||
|
||||
public CheckoutActionHandler(Long actionId, MachineActionStore store) {
|
||||
super(actionId, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() {
|
||||
ApplicationRepositoryDO repo = applicationRepositoryService.selectById(store.getRepoId());
|
||||
// 查询分支
|
||||
String fullBranchName = store.getBranchName();
|
||||
String remote = fullBranchName.substring(0, fullBranchName.indexOf("/"));
|
||||
String branchName = fullBranchName.substring(fullBranchName.indexOf("/") + 1);
|
||||
String commitId = store.getCommitId();
|
||||
String repoClonePath = store.getRepoClonePath();
|
||||
Files1.delete(repoClonePath);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF_2)
|
||||
.append(Utils.getStainKeyWords(" *** 检出url ", StainCode.GLOSS_BLUE))
|
||||
.append(Utils.getStainKeyWords(repo.getRepoUrl(), StainCode.GLOSS_CYAN))
|
||||
.append(Const.LF);
|
||||
log.append(Utils.getStainKeyWords(" *** 检出分支 ", StainCode.GLOSS_BLUE))
|
||||
.append(Utils.getStainKeyWords(fullBranchName, StainCode.GLOSS_CYAN))
|
||||
.append(Const.LF);
|
||||
log.append(Utils.getStainKeyWords(" *** commitId ", StainCode.GLOSS_BLUE))
|
||||
.append(Utils.getStainKeyWords(commitId, StainCode.GLOSS_CYAN))
|
||||
.append(Const.LF);
|
||||
log.append(Utils.getStainKeyWords(" *** 检出目录 ", StainCode.GLOSS_BLUE))
|
||||
.append(Utils.getStainKeyWords(repoClonePath, StainCode.GLOSS_CYAN))
|
||||
.append(Const.LF)
|
||||
.append(Utils.getStainKeyWords(" *** 开始检出", StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
// clone
|
||||
try {
|
||||
CloneCommand clone = Git.cloneRepository()
|
||||
.setURI(repo.getRepoUrl())
|
||||
.setDirectory(new File(repoClonePath))
|
||||
.setRemote(remote)
|
||||
.setBranch(branchName);
|
||||
// 设置密码
|
||||
String[] pair = applicationRepositoryService.getRepositoryUsernamePassword(repo);
|
||||
String username = pair[0];
|
||||
String password = pair[1];
|
||||
if (username != null) {
|
||||
clone.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password));
|
||||
}
|
||||
this.git = clone.call();
|
||||
this.appendLog(Utils.getStainKeyWords(" *** 检出完成", StainCode.GLOSS_GREEN) + Const.LF_3);
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.vcs(MessageConst.CHECKOUT_ERROR, e);
|
||||
}
|
||||
// 已停止则关闭
|
||||
if (terminated) {
|
||||
return;
|
||||
}
|
||||
// reset
|
||||
try {
|
||||
git.reset().setMode(ResetCommand.ResetType.HARD)
|
||||
.setRef(commitId)
|
||||
.call();
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.vcs(MessageConst.RESET_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
super.terminate();
|
||||
// 关闭git
|
||||
Streams.close(git);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
// 关闭git
|
||||
Streams.close(git);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.net.host.ssh.ExitCode;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import lombok.Getter;
|
||||
|
||||
public class CommandActionHandler extends AbstractActionHandler {
|
||||
|
||||
private CommandExecutor executor;
|
||||
|
||||
@Getter
|
||||
private Integer exitCode;
|
||||
|
||||
public CommandActionHandler(Long actionId, MachineActionStore store) {
|
||||
super(actionId, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
this.appendLog(Utils.getStainKeyWords("# 开始执行\n", StainCode.GLOSS_BLUE));
|
||||
// 打开executor
|
||||
this.executor = store.getSessionStore().getCommandExecutor(Strings.replaceCRLF(action.getActionCommand()));
|
||||
// 执行命令
|
||||
CommandExecutors.execCommand(executor, appender);
|
||||
this.exitCode = executor.getExitCode();
|
||||
if (!ExitCode.isSuccess(exitCode)) {
|
||||
throw Exceptions.execute("*** 命令执行失败 exitCode: " + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
super.terminate();
|
||||
// 关闭executor
|
||||
Streams.close(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String command) {
|
||||
executor.write(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
// 关闭executor
|
||||
Streams.close(executor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.kit.lang.function.select.Branches;
|
||||
import cn.orionsec.kit.lang.function.select.Selector;
|
||||
import cn.orionsec.ops.constant.app.ActionStatus;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.app.TransferMode;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface IActionHandler extends Executable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*
|
||||
* @return status
|
||||
* @see ActionStatus
|
||||
*/
|
||||
ActionStatus getStatus();
|
||||
|
||||
/**
|
||||
* 跳过
|
||||
*/
|
||||
void skip();
|
||||
|
||||
/**
|
||||
* 终止
|
||||
*/
|
||||
void terminate();
|
||||
|
||||
/**
|
||||
* 输入命令
|
||||
*
|
||||
* @param command command
|
||||
*/
|
||||
default void write(String command) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取退出码
|
||||
*
|
||||
* @return exitCode
|
||||
*/
|
||||
default Integer getExitCode() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建处理器
|
||||
*
|
||||
* @param actions actions
|
||||
* @param store store
|
||||
* @return handler
|
||||
*/
|
||||
static List<IActionHandler> createHandler(List<ApplicationActionLogDO> actions, MachineActionStore store) {
|
||||
return actions.stream()
|
||||
.map(action -> Selector.<ActionType, IActionHandler>of(ActionType.of(action.getActionType()))
|
||||
.test(Branches.eq(ActionType.BUILD_CHECKOUT)
|
||||
.then(() -> new CheckoutActionHandler(action.getId(), store)))
|
||||
.test(Branches.in(ActionType.BUILD_COMMAND, ActionType.RELEASE_COMMAND)
|
||||
.then(() -> new CommandActionHandler(action.getId(), store)))
|
||||
.test(Branches.eq(ActionType.RELEASE_TRANSFER)
|
||||
.then(() -> {
|
||||
if (TransferMode.SCP.getValue().equals(store.getTransferMode())) {
|
||||
return new ScpTransferActionHandler(action.getId(), store);
|
||||
} else {
|
||||
return new SftpTransferActionHandler(action.getId(), store);
|
||||
}
|
||||
}))
|
||||
.get())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.ops.constant.app.TransferMode;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class MachineActionStore {
|
||||
|
||||
/**
|
||||
* 引用id
|
||||
*/
|
||||
private Long relId;
|
||||
|
||||
/**
|
||||
* action
|
||||
*/
|
||||
private Map<Long, ApplicationActionLogDO> actions;
|
||||
|
||||
/**
|
||||
* 日志输出流
|
||||
*/
|
||||
private OutputStream superLogStream;
|
||||
|
||||
/**
|
||||
* 机器id
|
||||
*/
|
||||
private Long machineId;
|
||||
|
||||
/**
|
||||
* 机器用户
|
||||
*/
|
||||
private String machineUsername;
|
||||
|
||||
/**
|
||||
* 机器主机
|
||||
*/
|
||||
private String machineHost;
|
||||
|
||||
/**
|
||||
* 机器会话
|
||||
*/
|
||||
private SessionStore sessionStore;
|
||||
|
||||
/**
|
||||
* 版本id
|
||||
*
|
||||
* @see CheckoutActionHandler
|
||||
*/
|
||||
private Long repoId;
|
||||
|
||||
/**
|
||||
* 分支
|
||||
*
|
||||
* @see CheckoutActionHandler
|
||||
*/
|
||||
private String branchName;
|
||||
|
||||
/**
|
||||
* 提交版本
|
||||
*
|
||||
* @see CheckoutActionHandler
|
||||
*/
|
||||
private String commitId;
|
||||
|
||||
/**
|
||||
* 仓库 clone 路径
|
||||
*
|
||||
* @see CheckoutActionHandler
|
||||
*/
|
||||
private String repoClonePath;
|
||||
|
||||
/**
|
||||
* 构建产物文件
|
||||
*
|
||||
* @see SftpTransferActionHandler
|
||||
* @see ScpTransferActionHandler
|
||||
*/
|
||||
private String bundlePath;
|
||||
|
||||
/**
|
||||
* 产物传输路径
|
||||
*
|
||||
* @see SftpTransferActionHandler
|
||||
* @see ScpTransferActionHandler
|
||||
*/
|
||||
private String transferPath;
|
||||
|
||||
/**
|
||||
* 产物传输方式
|
||||
*
|
||||
* @see SftpTransferActionHandler
|
||||
* @see ScpTransferActionHandler
|
||||
* @see TransferMode
|
||||
*/
|
||||
private String transferMode;
|
||||
|
||||
public MachineActionStore() {
|
||||
this.actions = Maps.newLinkedMap();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.ssh.ExitCode;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.app.TransferMode;
|
||||
import cn.orionsec.ops.constant.command.CommandConst;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.env.EnvConst;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
|
||||
public class ScpTransferActionHandler extends AbstractActionHandler {
|
||||
|
||||
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private SessionStore session;
|
||||
|
||||
private CommandExecutor executor;
|
||||
|
||||
@Getter
|
||||
private Integer exitCode;
|
||||
|
||||
public ScpTransferActionHandler(Long actionId, MachineActionStore store) {
|
||||
super(actionId, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
// 检查文件
|
||||
String bundlePath = Files1.getPath(SystemEnvAttr.DIST_PATH.getValue(), store.getBundlePath());
|
||||
File bundleFile = new File(bundlePath);
|
||||
if (!bundleFile.exists()) {
|
||||
throw Exceptions.log("*** 产物文件不存在 " + bundlePath);
|
||||
}
|
||||
// 替换命令
|
||||
String scpCommand = Strings.def(action.getActionCommand(), CommandConst.SCP_TRANSFER_DEFAULT);
|
||||
Map<String, String> params = Maps.newMap();
|
||||
params.put(EnvConst.BUNDLE_PATH, bundlePath);
|
||||
// 目标文件有空格需要转义空格为 \\
|
||||
params.put(EnvConst.TRANSFER_PATH, store.getTransferPath().replaceAll(Strings.SPACE, "\\\\\\\\ "));
|
||||
params.put(EnvConst.TARGET_USERNAME, store.getMachineUsername());
|
||||
params.put(EnvConst.TARGET_HOST, store.getMachineHost());
|
||||
scpCommand = Strings.format(scpCommand, EnvConst.SYMBOL, params);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF)
|
||||
.append(Utils.getStainKeyWords("# 执行 scp 传输命令", StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF)
|
||||
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
|
||||
.append(Utils.getEndLfWithEof(scpCommand))
|
||||
.append(StainCode.SUFFIX);
|
||||
this.appendLog(log.toString());
|
||||
// 打开session
|
||||
this.session = machineInfoService.openSessionStore(Const.HOST_MACHINE_ID);
|
||||
// 打开executor
|
||||
this.executor = session.getCommandExecutor(Strings.replaceCRLF(scpCommand));
|
||||
// 执行命令
|
||||
CommandExecutors.execCommand(executor, appender);
|
||||
this.exitCode = executor.getExitCode();
|
||||
this.appendLog(Const.LF);
|
||||
if (!ExitCode.isSuccess(exitCode)) {
|
||||
throw Exceptions.execute("*** 命令执行失败 exitCode: " + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String command) {
|
||||
executor.write(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
super.terminate();
|
||||
// 关闭executor
|
||||
Streams.close(executor);
|
||||
// 关闭宿主机session
|
||||
Streams.close(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
// 关闭executor
|
||||
Streams.close(executor);
|
||||
// 关闭宿主机session
|
||||
Streams.close(session);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.action;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.app.TransferMode;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.service.api.MachineEnvService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SftpTransferActionHandler extends AbstractActionHandler {
|
||||
|
||||
private static final String SPACE = " ";
|
||||
|
||||
protected static MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
|
||||
|
||||
private SftpExecutor executor;
|
||||
|
||||
public SftpTransferActionHandler(Long actionId, MachineActionStore store) {
|
||||
super(actionId, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
// 检查文件
|
||||
String bundlePath = Files1.getPath(SystemEnvAttr.DIST_PATH.getValue(), store.getBundlePath());
|
||||
File bundleFile = new File(bundlePath);
|
||||
if (!bundleFile.exists()) {
|
||||
throw Exceptions.log("*** 产物文件不存在 " + bundlePath);
|
||||
}
|
||||
// 打开executor
|
||||
String charset = machineEnvService.getSftpCharset(store.getMachineId());
|
||||
this.executor = store.getSessionStore().getSftpExecutor(charset);
|
||||
executor.connect();
|
||||
// 拼接删除日志
|
||||
String transferPath = store.getTransferPath();
|
||||
String bundleAbsolutePath = bundleFile.getAbsolutePath();
|
||||
// 拼接头文件
|
||||
StringBuilder headerLog = new StringBuilder(Const.LF)
|
||||
.append(SPACE)
|
||||
.append(Utils.getStainKeyWords("开始传输文件", StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF)
|
||||
.append(SPACE)
|
||||
.append(Utils.getStainKeyWords("source: ", StainCode.GLOSS_GREEN))
|
||||
.append(Utils.getStainKeyWords(bundleAbsolutePath, StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF)
|
||||
.append(SPACE)
|
||||
.append(Utils.getStainKeyWords("target: ", StainCode.GLOSS_GREEN))
|
||||
.append(Utils.getStainKeyWords(transferPath, StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF_2);
|
||||
headerLog.append(StainCode.prefix(StainCode.GLOSS_GREEN))
|
||||
.append(SPACE)
|
||||
.append("类型")
|
||||
.append(SPACE)
|
||||
.append(" target")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Const.LF);
|
||||
this.appendLog(headerLog.toString());
|
||||
// 转化文件
|
||||
Map<File, String> transferFiles = this.convertFile(bundleFile, transferPath);
|
||||
for (Map.Entry<File, String> entity : transferFiles.entrySet()) {
|
||||
File localFile = entity.getKey();
|
||||
String remoteFile = entity.getValue();
|
||||
// 文件夹则创建
|
||||
if (localFile.isDirectory()) {
|
||||
StringBuilder createDirLog = new StringBuilder(SPACE)
|
||||
.append(Utils.getStainKeyWords("mkdir", StainCode.GLOSS_GREEN))
|
||||
.append(SPACE)
|
||||
.append(Utils.getStainKeyWords(remoteFile, StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(createDirLog.toString());
|
||||
executor.makeDirectories(remoteFile);
|
||||
continue;
|
||||
}
|
||||
// 文件则传输
|
||||
StringBuilder transferLog = new StringBuilder(SPACE)
|
||||
.append(Utils.getStainKeyWords("touch", StainCode.GLOSS_GREEN))
|
||||
.append(SPACE)
|
||||
.append(Utils.getStainKeyWords(remoteFile, StainCode.GLOSS_BLUE))
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append(" (")
|
||||
.append(Files1.getSize(localFile.length()))
|
||||
.append(")")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Const.LF);
|
||||
this.appendLog(transferLog.toString());
|
||||
executor.uploadFile(remoteFile, Files1.openInputStreamFast(localFile), true);
|
||||
}
|
||||
this.appendLog(Const.LF);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转化文件
|
||||
*
|
||||
* @param bundleFile 打包文件
|
||||
* @param transferPath 传输目录
|
||||
* @return transferFiles
|
||||
*/
|
||||
private Map<File, String> convertFile(File bundleFile, String transferPath) {
|
||||
Map<File, String> map = Maps.newLinkedMap();
|
||||
if (bundleFile.isFile()) {
|
||||
map.put(bundleFile, transferPath);
|
||||
return map;
|
||||
}
|
||||
// 如果是文件夹则需要截取
|
||||
String bundleFileAbsolutePath = bundleFile.getAbsolutePath();
|
||||
List<File> transferFiles = Files1.listFiles(bundleFile, true, true);
|
||||
for (File transferFile : transferFiles) {
|
||||
String remoteFile = Files1.getPath(transferPath, transferFile.getAbsolutePath().substring(bundleFileAbsolutePath.length() + 1));
|
||||
map.put(transferFile, remoteFile);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
super.terminate();
|
||||
// 关闭executor
|
||||
Streams.close(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
// 关闭executor
|
||||
Streams.close(executor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.build;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.ops.handler.app.machine.IMachineProcessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class BuildSessionHolder {
|
||||
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
private final ConcurrentHashMap<Long, IMachineProcessor> session = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加 session
|
||||
*
|
||||
* @param processor processor
|
||||
*/
|
||||
public void addSession(Long id, IMachineProcessor processor) {
|
||||
session.put(id, processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public IMachineProcessor getSession(Long id) {
|
||||
return session.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 session
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
public void removeSession(Long id) {
|
||||
session.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.machine;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.Letters;
|
||||
import cn.orionsec.kit.lang.exception.DisabledException;
|
||||
import cn.orionsec.kit.lang.exception.LogException;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.app.ActionStatus;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.handler.app.action.IActionHandler;
|
||||
import cn.orionsec.ops.handler.tail.TailSessionHolder;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractMachineProcessor implements IMachineProcessor {
|
||||
|
||||
private static final TailSessionHolder tailSessionHolder = SpringHolder.getBean(TailSessionHolder.class);
|
||||
|
||||
protected Long id;
|
||||
|
||||
protected String logAbsolutePath;
|
||||
|
||||
protected OutputStream logStream;
|
||||
|
||||
protected Date startTime, endTime;
|
||||
|
||||
/**
|
||||
* 处理器
|
||||
*/
|
||||
protected List<IActionHandler> handlerList;
|
||||
|
||||
/**
|
||||
* 是否已终止
|
||||
*/
|
||||
protected volatile boolean terminated;
|
||||
|
||||
public AbstractMachineProcessor(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// 检查状态
|
||||
if (!this.checkCanRunnable()) {
|
||||
return;
|
||||
}
|
||||
Exception ex = null;
|
||||
boolean isMainError = false;
|
||||
// 执行
|
||||
try {
|
||||
// 更新状态
|
||||
this.updateStatus(MachineProcessorStatus.RUNNABLE);
|
||||
// 打开日志
|
||||
this.openLogger();
|
||||
// 打开机器连接
|
||||
this.openMachineSession();
|
||||
// 执行
|
||||
for (IActionHandler handler : handlerList) {
|
||||
if (ex == null && !terminated) {
|
||||
try {
|
||||
// 执行
|
||||
handler.exec();
|
||||
} catch (Exception e) {
|
||||
// 强制停止的异常不算异常
|
||||
if (!terminated) {
|
||||
ex = e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 跳过
|
||||
handler.skip();
|
||||
}
|
||||
}
|
||||
// 完成回调
|
||||
this.completeCallback(ex);
|
||||
} catch (Exception e) {
|
||||
// 异常
|
||||
ex = e;
|
||||
isMainError = true;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 成功回调
|
||||
this.successCallback();
|
||||
} else if (ex instanceof DisabledException) {
|
||||
// 机器未启用回调
|
||||
this.machineDisableCallback();
|
||||
} else {
|
||||
// 执行失败回调
|
||||
this.exceptionCallback(isMainError, ex);
|
||||
}
|
||||
} finally {
|
||||
// 释放资源
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可执行
|
||||
*
|
||||
* @return 是否可执行
|
||||
*/
|
||||
protected abstract boolean checkCanRunnable();
|
||||
|
||||
/**
|
||||
* 打开日志
|
||||
*/
|
||||
protected void openLogger() {
|
||||
String logPath = Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), this.getLogPath());
|
||||
File logFile = new File(logPath);
|
||||
Files1.touch(logFile);
|
||||
this.logStream = Files1.openOutputStreamFastSafe(logFile);
|
||||
this.logAbsolutePath = logFile.getAbsolutePath();
|
||||
// 拼接开始日志
|
||||
this.appendStartedLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志文件路径
|
||||
*
|
||||
* @return logPath
|
||||
*/
|
||||
protected abstract String getLogPath();
|
||||
|
||||
/**
|
||||
* 打开session
|
||||
*/
|
||||
protected abstract void openMachineSession();
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
protected abstract void updateStatus(MachineProcessorStatus status);
|
||||
|
||||
/**
|
||||
* 拼接开始日志
|
||||
*/
|
||||
protected abstract void appendStartedLog();
|
||||
|
||||
/**
|
||||
* 完成回调
|
||||
*
|
||||
* @param e e
|
||||
*/
|
||||
protected void completeCallback(Exception e) {
|
||||
log.info("机器任务执行-完成 relId: {}", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
protected void terminatedCallback() {
|
||||
log.info("机器任务执行-停止 relId: {}", id);
|
||||
// 修改状态
|
||||
this.updateStatus(MachineProcessorStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF_2)
|
||||
.append(Utils.getStainKeyWords("# 主机任务手动停止", StainCode.GLOSS_YELLOW))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功回调
|
||||
*/
|
||||
protected void successCallback() {
|
||||
log.info("机器任务执行-成功 relId: {}", id);
|
||||
// 修改状态
|
||||
this.updateStatus(MachineProcessorStatus.FINISH);
|
||||
long used = endTime.getTime() - startTime.getTime();
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF_2)
|
||||
.append(Utils.getStainKeyWords("# 主机任务执行完成", StainCode.GLOSS_GREEN))
|
||||
.append(Const.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(" used ")
|
||||
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
|
||||
.append(" (")
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append(used)
|
||||
.append("ms")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(")\n");
|
||||
// 拼接日志
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器未启用回调
|
||||
*/
|
||||
private void machineDisableCallback() {
|
||||
log.info("机器任务执行-机器未启用 relId: {}", id);
|
||||
// 更新状态
|
||||
this.updateStatus(MachineProcessorStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF_2)
|
||||
.append(Utils.getStainKeyWords("# 主机任务执行机器未启用", StainCode.GLOSS_YELLOW))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*
|
||||
* @param isMainError isMainError
|
||||
* @param ex ex
|
||||
*/
|
||||
protected void exceptionCallback(boolean isMainError, Exception ex) {
|
||||
log.error("机器任务执行-失败 relId: {}, isMainError: {}", id, isMainError, ex);
|
||||
// 更新状态
|
||||
this.updateStatus(MachineProcessorStatus.FAILURE);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF_2)
|
||||
.append(Utils.getStainKeyWords("# 主机任务执行失败", StainCode.GLOSS_RED))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF);
|
||||
// 拼接异常
|
||||
if (isMainError) {
|
||||
log.append(Const.LF);
|
||||
if (ex instanceof LogException) {
|
||||
log.append(Utils.getStainKeyWords(ex.getMessage(), StainCode.GLOSS_RED));
|
||||
} else {
|
||||
log.append(Exceptions.getStackTraceAsString(ex));
|
||||
}
|
||||
log.append(Const.LF);
|
||||
}
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接日志
|
||||
*
|
||||
* @param log log
|
||||
*/
|
||||
@SneakyThrows
|
||||
protected void appendLog(String log) {
|
||||
logStream.write(Strings.bytes(log));
|
||||
logStream.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
// 设置状态为已停止
|
||||
this.terminated = true;
|
||||
// 结束正在执行的action
|
||||
Lists.stream(handlerList)
|
||||
.filter(s -> ActionStatus.RUNNABLE.equals(s.getStatus()))
|
||||
.forEach(IActionHandler::terminate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String command) {
|
||||
Lists.stream(handlerList)
|
||||
.filter(s -> ActionStatus.RUNNABLE.equals(s.getStatus()))
|
||||
.forEach(s -> s.write(command));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 关闭日志流
|
||||
Streams.close(logStream);
|
||||
// 异步关闭正在tail的日志
|
||||
tailSessionHolder.asyncCloseTailFile(Const.HOST_MACHINE_ID, logAbsolutePath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.machine;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.io.compress.CompressTypeEnum;
|
||||
import cn.orionsec.kit.lang.utils.io.compress.FileCompressor;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.app.ActionType;
|
||||
import cn.orionsec.ops.constant.app.ApplicationEnvAttr;
|
||||
import cn.orionsec.ops.constant.app.BuildStatus;
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.message.MessageType;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.dao.ApplicationBuildDAO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationBuildDO;
|
||||
import cn.orionsec.ops.handler.app.action.IActionHandler;
|
||||
import cn.orionsec.ops.handler.app.action.MachineActionStore;
|
||||
import cn.orionsec.ops.handler.app.build.BuildSessionHolder;
|
||||
import cn.orionsec.ops.service.api.ApplicationActionLogService;
|
||||
import cn.orionsec.ops.service.api.ApplicationEnvService;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.service.api.WebSideMessageService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class BuildMachineProcessor extends AbstractMachineProcessor implements Executable {
|
||||
|
||||
private static final ApplicationBuildDAO applicationBuildDAO = SpringHolder.getBean(ApplicationBuildDAO.class);
|
||||
|
||||
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private static final ApplicationActionLogService applicationActionLogService = SpringHolder.getBean(ApplicationActionLogService.class);
|
||||
|
||||
private static final ApplicationEnvService applicationEnvService = SpringHolder.getBean(ApplicationEnvService.class);
|
||||
|
||||
private static final BuildSessionHolder buildSessionHolder = SpringHolder.getBean(BuildSessionHolder.class);
|
||||
|
||||
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
|
||||
|
||||
private final MachineActionStore store;
|
||||
|
||||
private ApplicationBuildDO record;
|
||||
|
||||
public BuildMachineProcessor(Long id) {
|
||||
super(id);
|
||||
this.store = new MachineActionStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
log.info("应用构建任务执行提交 buildId: {}", id);
|
||||
Threads.start(this, SchedulerPools.APP_BUILD_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("应用构建任务执行开始 buildId: {}", id);
|
||||
// 初始化数据
|
||||
this.initData();
|
||||
// 执行
|
||||
super.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
private void initData() {
|
||||
// 查询build
|
||||
this.record = applicationBuildDAO.selectById(id);
|
||||
log.info("应用构建任务-获取数据-build buildId: {}, record: {}", id, JSON.toJSONString(record));
|
||||
// 检查状态
|
||||
if (record == null || !BuildStatus.WAIT.getStatus().equals(record.getBuildStatus())) {
|
||||
return;
|
||||
}
|
||||
// 查询action
|
||||
List<ApplicationActionLogDO> actions = applicationActionLogService.selectActionByRelId(id, StageType.BUILD);
|
||||
actions.forEach(s -> store.getActions().put(s.getId(), s));
|
||||
log.info("应用构建任务-获取数据-action buildId: {}, actions: {}", id, JSON.toJSONString(actions));
|
||||
// 插入store
|
||||
Long repoId = record.getRepoId();
|
||||
store.setRelId(id);
|
||||
store.setRepoId(repoId);
|
||||
store.setBranchName(record.getBranchName());
|
||||
store.setCommitId(record.getCommitId());
|
||||
if (repoId != null) {
|
||||
String repoClonePath = Files1.getPath(SystemEnvAttr.REPO_PATH.getValue(), repoId + "/" + record.getId());
|
||||
store.setRepoClonePath(repoClonePath);
|
||||
}
|
||||
// 创建handler
|
||||
this.handlerList = IActionHandler.createHandler(actions, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkCanRunnable() {
|
||||
return record != null && BuildStatus.WAIT.getStatus().equals(record.getBuildStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void completeCallback(Exception e) {
|
||||
super.completeCallback(e);
|
||||
if (e == null && !terminated) {
|
||||
// 复制产物文件
|
||||
this.copyBundleFile();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void successCallback() {
|
||||
// 完成回调
|
||||
super.successCallback();
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, record.getId());
|
||||
params.put(EventKeys.SEQ, record.getBuildSeq());
|
||||
params.put(EventKeys.PROFILE_NAME, record.getProfileName());
|
||||
params.put(EventKeys.APP_NAME, record.getAppName());
|
||||
params.put(EventKeys.BUILD_SEQ, record.getBuildSeq());
|
||||
webSideMessageService.addMessage(MessageType.BUILD_SUCCESS, record.getId(), record.getCreateUserId(), record.getCreateUserName(), params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exceptionCallback(boolean isMainError, Exception ex) {
|
||||
super.exceptionCallback(isMainError, ex);
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, record.getId());
|
||||
params.put(EventKeys.SEQ, record.getBuildSeq());
|
||||
params.put(EventKeys.APP_NAME, record.getAppName());
|
||||
params.put(EventKeys.PROFILE_NAME, record.getProfileName());
|
||||
params.put(EventKeys.BUILD_SEQ, record.getBuildSeq());
|
||||
webSideMessageService.addMessage(MessageType.BUILD_FAILURE, record.getId(), record.getCreateUserId(), record.getCreateUserName(), params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void openLogger() {
|
||||
super.openLogger();
|
||||
store.setSuperLogStream(this.logStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制产物文件
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void copyBundleFile() {
|
||||
// 查询应用产物目录
|
||||
String bundlePath = applicationEnvService.getAppEnvValue(record.getAppId(), record.getProfileId(), ApplicationEnvAttr.BUNDLE_PATH.getKey());
|
||||
if (!bundlePath.startsWith(Const.SLASH) && !Files1.isWindowsPath(bundlePath) && store.getRepoClonePath() != null) {
|
||||
// 基于代码目录的相对路径
|
||||
bundlePath = Files1.getPath(store.getRepoClonePath(), bundlePath);
|
||||
}
|
||||
// 检查产物文件是否存在
|
||||
File bundleFile = new File(bundlePath);
|
||||
if (!bundleFile.exists()) {
|
||||
throw Exceptions.log("***** 构建产物不存在 " + bundlePath);
|
||||
}
|
||||
// 复制到dist目录下
|
||||
String copyBundlePath = Files1.getPath(SystemEnvAttr.DIST_PATH.getValue(), record.getBundlePath());
|
||||
StringBuilder copyLog = new StringBuilder(Const.LF_3)
|
||||
.append(StainCode.prefix(StainCode.GLOSS_GREEN))
|
||||
.append("***** 已生成产物文件 ")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Utils.getStainKeyWords(copyBundlePath, StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(copyLog.toString());
|
||||
if (bundleFile.isFile()) {
|
||||
Files1.copy(bundleFile, new File(copyBundlePath));
|
||||
} else {
|
||||
// 复制文件夹
|
||||
Files1.copyDir(bundleFile, new File(copyBundlePath), false);
|
||||
// 文件夹打包
|
||||
String compressFile = copyBundlePath + "." + Const.SUFFIX_ZIP;
|
||||
FileCompressor compressor = CompressTypeEnum.ZIP.compressor().get();
|
||||
compressor.addFile(bundleFile);
|
||||
compressor.setAbsoluteCompressPath(compressFile);
|
||||
compressor.compress();
|
||||
StringBuilder compressLog = new StringBuilder()
|
||||
.append(StainCode.prefix(StainCode.GLOSS_GREEN))
|
||||
.append("***** 已生成产物文件zip ")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Utils.getStainKeyWords(compressFile, StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(compressLog.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLogPath() {
|
||||
return record.getLogPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void openMachineSession() {
|
||||
boolean hasCommand = store.getActions().values().stream()
|
||||
.map(ApplicationActionLogDO::getActionType)
|
||||
.anyMatch(ActionType.BUILD_COMMAND.getType()::equals);
|
||||
if (!hasCommand) {
|
||||
return;
|
||||
}
|
||||
// 打开session
|
||||
SessionStore sessionStore = machineInfoService.openSessionStore(Const.HOST_MACHINE_ID);
|
||||
store.setMachineId(Const.HOST_MACHINE_ID);
|
||||
store.setSessionStore(sessionStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateStatus(MachineProcessorStatus status) {
|
||||
Date now = new Date();
|
||||
ApplicationBuildDO update = new ApplicationBuildDO();
|
||||
update.setId(id);
|
||||
update.setBuildStatus(BuildStatus.valueOf(status.name()).getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
this.startTime = now;
|
||||
update.setBuildStartTime(now);
|
||||
// 添加session
|
||||
buildSessionHolder.addSession(id, this);
|
||||
break;
|
||||
case FINISH:
|
||||
case FAILURE:
|
||||
case TERMINATED:
|
||||
this.endTime = now;
|
||||
update.setBuildEndTime(now);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// 更新
|
||||
applicationBuildDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendStartedLog() {
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Utils.getStainKeyWords("# 开始执行主机构建任务 ", StainCode.GLOSS_GREEN))
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append("#").append(record.getBuildSeq())
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Const.LF);
|
||||
log.append("构建应用: ")
|
||||
.append(Utils.getStainKeyWords(record.getAppName(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("构建环境: ")
|
||||
.append(Utils.getStainKeyWords(record.getProfileName(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("执行用户: ")
|
||||
.append(Utils.getStainKeyWords(record.getCreateUserName(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("开始时间: ")
|
||||
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
if (!Strings.isBlank(record.getDescription())) {
|
||||
log.append("构建描述: ")
|
||||
.append(Utils.getStainKeyWords(record.getDescription(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
}
|
||||
if (!Strings.isBlank(record.getBranchName())) {
|
||||
log.append("branch: ")
|
||||
.append(Utils.getStainKeyWords(record.getBranchName(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("commit: ")
|
||||
.append(Utils.getStainKeyWords(record.getCommitId(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
}
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 释放资源
|
||||
super.close();
|
||||
// 释放连接
|
||||
Streams.close(store.getSessionStore());
|
||||
// 移除session
|
||||
buildSessionHolder.removeSession(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.machine;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
|
||||
public interface IMachineProcessor extends Runnable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 终止
|
||||
*/
|
||||
void terminate();
|
||||
|
||||
/**
|
||||
* 输入命令
|
||||
*
|
||||
* @param command command
|
||||
*/
|
||||
void write(String command);
|
||||
|
||||
/**
|
||||
* 跳过
|
||||
*/
|
||||
default void skip() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.machine;
|
||||
|
||||
public enum MachineProcessorStatus {
|
||||
|
||||
/**
|
||||
* 未开始
|
||||
*/
|
||||
WAIT,
|
||||
|
||||
/**
|
||||
* 进行中
|
||||
*/
|
||||
RUNNABLE,
|
||||
|
||||
/**
|
||||
* 已完成
|
||||
*/
|
||||
FINISH,
|
||||
|
||||
/**
|
||||
* 执行失败
|
||||
*/
|
||||
FAILURE,
|
||||
|
||||
/**
|
||||
* 已跳过
|
||||
*/
|
||||
SKIPPED,
|
||||
|
||||
/**
|
||||
* 已终止
|
||||
*/
|
||||
TERMINATED,
|
||||
|
||||
;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.machine;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.app.ActionStatus;
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.dao.ApplicationMachineDAO;
|
||||
import cn.orionsec.ops.dao.ApplicationReleaseMachineDAO;
|
||||
import cn.orionsec.ops.entity.domain.*;
|
||||
import cn.orionsec.ops.handler.app.action.IActionHandler;
|
||||
import cn.orionsec.ops.handler.app.action.MachineActionStore;
|
||||
import cn.orionsec.ops.service.api.ApplicationActionLogService;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class ReleaseMachineProcessor extends AbstractMachineProcessor {
|
||||
|
||||
protected static ApplicationReleaseMachineDAO applicationReleaseMachineDAO = SpringHolder.getBean(ApplicationReleaseMachineDAO.class);
|
||||
|
||||
protected static ApplicationMachineDAO applicationMachineDAO = SpringHolder.getBean(ApplicationMachineDAO.class);
|
||||
|
||||
protected static ApplicationActionLogService applicationActionLogService = SpringHolder.getBean(ApplicationActionLogService.class);
|
||||
|
||||
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private final ApplicationReleaseDO release;
|
||||
|
||||
private final ApplicationReleaseMachineDO releaseMachine;
|
||||
|
||||
private final MachineActionStore store;
|
||||
|
||||
@Getter
|
||||
private volatile ActionStatus status;
|
||||
|
||||
public ReleaseMachineProcessor(ApplicationReleaseDO release, ApplicationReleaseMachineDO releaseMachine) {
|
||||
super(releaseMachine.getId());
|
||||
this.release = release;
|
||||
this.releaseMachine = releaseMachine;
|
||||
this.status = ActionStatus.of(releaseMachine.getRunStatus());
|
||||
this.store = new MachineActionStore();
|
||||
store.setRelId(releaseMachine.getId());
|
||||
store.setMachineId(releaseMachine.getMachineId());
|
||||
store.setBundlePath(release.getBundlePath());
|
||||
store.setTransferPath(release.getTransferPath());
|
||||
store.setTransferMode(release.getTransferMode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("应用发布任务执行开始 releaseId: {}", id);
|
||||
// 初始化数据
|
||||
this.initData();
|
||||
// 执行
|
||||
super.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据
|
||||
*/
|
||||
private void initData() {
|
||||
// 查询机器发布操作
|
||||
List<ApplicationActionLogDO> actions = applicationActionLogService.selectActionByRelId(id, StageType.RELEASE);
|
||||
actions.forEach(s -> store.getActions().put(s.getId(), s));
|
||||
log.info("应用发布器-获取数据-action releaseId: {}, actions: {}", id, JSON.toJSONString(actions));
|
||||
// 创建handler
|
||||
this.handlerList = IActionHandler.createHandler(actions, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip() {
|
||||
if (ActionStatus.WAIT.equals(status)) {
|
||||
// 只能跳过等待中的任务
|
||||
this.updateStatus(MachineProcessorStatus.SKIPPED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkCanRunnable() {
|
||||
return ActionStatus.WAIT.equals(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void openLogger() {
|
||||
super.openLogger();
|
||||
store.setSuperLogStream(this.logStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLogPath() {
|
||||
return releaseMachine.getLogPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void successCallback() {
|
||||
// 完成回调
|
||||
super.successCallback();
|
||||
// 更新应用机器发布版本
|
||||
this.updateAppMachineVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exceptionCallback(boolean isMainError, Exception ex) {
|
||||
super.exceptionCallback(isMainError, ex);
|
||||
throw Exceptions.runtime(ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void openMachineSession() {
|
||||
// 打开目标机器session
|
||||
Long machineId = releaseMachine.getMachineId();
|
||||
MachineInfoDO machine = machineInfoService.selectById(machineId);
|
||||
SessionStore sessionStore = machineInfoService.openSessionStore(machine);
|
||||
store.setSessionStore(sessionStore);
|
||||
store.setMachineUsername(machine.getUsername());
|
||||
store.setMachineHost(machine.getMachineHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateStatus(MachineProcessorStatus processorStatus) {
|
||||
this.status = ActionStatus.valueOf(processorStatus.name());
|
||||
Date now = new Date();
|
||||
ApplicationReleaseMachineDO update = new ApplicationReleaseMachineDO();
|
||||
update.setId(id);
|
||||
update.setRunStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (processorStatus) {
|
||||
case RUNNABLE:
|
||||
this.startTime = now;
|
||||
update.setStartTime(now);
|
||||
break;
|
||||
case FINISH:
|
||||
case TERMINATED:
|
||||
case FAILURE:
|
||||
this.endTime = now;
|
||||
update.setEndTime(now);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
applicationReleaseMachineDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendStartedLog() {
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Utils.getStainKeyWords("# 开始执行主机发布任务", StainCode.GLOSS_GREEN))
|
||||
.append(Const.LF);
|
||||
log.append("机器名称: ")
|
||||
.append(Utils.getStainKeyWords(releaseMachine.getMachineName(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("发布主机: ")
|
||||
.append(Utils.getStainKeyWords(releaseMachine.getMachineHost(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("开始时间: ")
|
||||
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新应用机器版本
|
||||
*/
|
||||
private void updateAppMachineVersion() {
|
||||
ApplicationMachineDO update = new ApplicationMachineDO();
|
||||
update.setAppId(release.getAppId());
|
||||
update.setProfileId(release.getProfileId());
|
||||
update.setMachineId(releaseMachine.getMachineId());
|
||||
update.setReleaseId(release.getId());
|
||||
update.setBuildId(release.getBuildId());
|
||||
update.setBuildSeq(release.getBuildSeq());
|
||||
applicationMachineDAO.updateAppVersion(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 释放资源
|
||||
super.close();
|
||||
// 释放连接
|
||||
Streams.close(store.getSessionStore());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
|
||||
public interface IPipelineProcessor extends Executable, Runnable {
|
||||
|
||||
/**
|
||||
* 获取明细id
|
||||
*
|
||||
* @return taskId
|
||||
*/
|
||||
Long getTaskId();
|
||||
|
||||
/**
|
||||
* 停止执行
|
||||
*/
|
||||
void terminate();
|
||||
|
||||
/**
|
||||
* 停止执行详情
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
void terminateDetail(Long id);
|
||||
|
||||
/**
|
||||
* 跳过执行详情
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
void skipDetail(Long id);
|
||||
|
||||
/**
|
||||
* 获取流水线执行器
|
||||
*
|
||||
* @param id id
|
||||
* @return 流水线执行器
|
||||
*/
|
||||
static IPipelineProcessor with(Long id) {
|
||||
return new PipelineProcessor(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.app.PipelineDetailStatus;
|
||||
import cn.orionsec.ops.constant.app.PipelineStatus;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.message.MessageType;
|
||||
import cn.orionsec.ops.dao.ApplicationPipelineTaskDAO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
|
||||
import cn.orionsec.ops.handler.app.pipeline.stage.IStageHandler;
|
||||
import cn.orionsec.ops.service.api.ApplicationPipelineTaskDetailService;
|
||||
import cn.orionsec.ops.service.api.WebSideMessageService;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class PipelineProcessor implements IPipelineProcessor {
|
||||
|
||||
private static final ApplicationPipelineTaskDAO applicationPipelineTaskDAO = SpringHolder.getBean(ApplicationPipelineTaskDAO.class);
|
||||
|
||||
private static final ApplicationPipelineTaskDetailService applicationPipelineTaskDetailService = SpringHolder.getBean(ApplicationPipelineTaskDetailService.class);
|
||||
|
||||
private static final PipelineSessionHolder pipelineSessionHolder = SpringHolder.getBean(PipelineSessionHolder.class);
|
||||
|
||||
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
|
||||
|
||||
@Getter
|
||||
private final Long taskId;
|
||||
|
||||
private ApplicationPipelineTaskDO task;
|
||||
|
||||
private final Map<Long, IStageHandler> stageHandlers;
|
||||
|
||||
private volatile IStageHandler currentHandler;
|
||||
|
||||
private volatile boolean terminated;
|
||||
|
||||
public PipelineProcessor(Long taskId) {
|
||||
this.taskId = taskId;
|
||||
this.stageHandlers = Maps.newLinkedMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
log.info("已提交应用流水线任务 id: {}", taskId);
|
||||
Threads.start(this, SchedulerPools.PIPELINE_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("开始执行应用流水线 id: {}", taskId);
|
||||
Exception ex = null;
|
||||
try {
|
||||
// 获取流水线数据
|
||||
this.getPipelineData();
|
||||
// 检查状态
|
||||
if (task != null && !PipelineStatus.WAIT_RUNNABLE.getStatus().equals(task.getExecStatus())
|
||||
&& !PipelineStatus.WAIT_SCHEDULE.getStatus().equals(task.getExecStatus())) {
|
||||
return;
|
||||
}
|
||||
// 修改状态
|
||||
this.updateStatus(PipelineStatus.RUNNABLE);
|
||||
// 添加会话
|
||||
pipelineSessionHolder.addSession(this);
|
||||
// 执行
|
||||
this.handlerPipeline();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
try {
|
||||
// 回调
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 成功回调
|
||||
this.completeCallback();
|
||||
} else {
|
||||
// 异常回调
|
||||
this.exceptionCallback(ex);
|
||||
}
|
||||
} finally {
|
||||
pipelineSessionHolder.removeSession(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流水线数据
|
||||
*/
|
||||
private void getPipelineData() {
|
||||
// 获取主表
|
||||
this.task = applicationPipelineTaskDAO.selectById(taskId);
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
PipelineStatus status = PipelineStatus.of(task.getExecStatus());
|
||||
if (!PipelineStatus.WAIT_RUNNABLE.equals(status)
|
||||
&& !PipelineStatus.WAIT_SCHEDULE.equals(status)) {
|
||||
throw Exceptions.argument(MessageConst.ILLEGAL_STATUS);
|
||||
}
|
||||
// 获取详情
|
||||
List<ApplicationPipelineTaskDetailDO> details = applicationPipelineTaskDetailService.selectTaskDetails(taskId);
|
||||
for (ApplicationPipelineTaskDetailDO detail : details) {
|
||||
stageHandlers.put(detail.getId(), IStageHandler.with(task, detail));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流水线操作
|
||||
*
|
||||
* @throws Exception exception
|
||||
*/
|
||||
private void handlerPipeline() throws Exception {
|
||||
Exception ex = null;
|
||||
Collection<IStageHandler> handlers = stageHandlers.values();
|
||||
for (IStageHandler stageHandler : handlers) {
|
||||
this.currentHandler = stageHandler;
|
||||
// 停止或异常则跳过
|
||||
if (terminated || ex != null) {
|
||||
stageHandler.skip();
|
||||
continue;
|
||||
}
|
||||
// 执行
|
||||
try {
|
||||
stageHandler.exec();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
}
|
||||
this.currentHandler = null;
|
||||
// 异常返回
|
||||
if (ex != null) {
|
||||
throw ex;
|
||||
}
|
||||
// 全部停止
|
||||
final boolean allTerminated = handlers.stream()
|
||||
.map(IStageHandler::getStatus)
|
||||
.filter(s -> !PipelineDetailStatus.SKIPPED.equals(s))
|
||||
.allMatch(PipelineDetailStatus.TERMINATED::equals);
|
||||
if (allTerminated) {
|
||||
this.terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
private void terminatedCallback() {
|
||||
log.info("应用流水线执行停止 id: {}", taskId);
|
||||
this.updateStatus(PipelineStatus.TERMINATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成回调
|
||||
*/
|
||||
private void completeCallback() {
|
||||
log.info("应用流水线执行完成 id: {}", taskId);
|
||||
this.updateStatus(PipelineStatus.FINISH);
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, task.getId());
|
||||
params.put(EventKeys.NAME, task.getPipelineName());
|
||||
params.put(EventKeys.TITLE, task.getExecTitle());
|
||||
webSideMessageService.addMessage(MessageType.PIPELINE_EXEC_SUCCESS, task.getId(), task.getExecUserId(), task.getExecUserName(), params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*
|
||||
* @param ex ex
|
||||
*/
|
||||
private void exceptionCallback(Exception ex) {
|
||||
log.error("应用流水线执行失败 id: {}", taskId, ex);
|
||||
this.updateStatus(PipelineStatus.FAILURE);
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, task.getId());
|
||||
params.put(EventKeys.NAME, task.getPipelineName());
|
||||
params.put(EventKeys.TITLE, task.getExecTitle());
|
||||
webSideMessageService.addMessage(MessageType.PIPELINE_EXEC_FAILURE, task.getId(), task.getExecUserId(), task.getExecUserName(), params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
private void updateStatus(PipelineStatus status) {
|
||||
Date now = new Date();
|
||||
ApplicationPipelineTaskDO update = new ApplicationPipelineTaskDO();
|
||||
update.setId(taskId);
|
||||
update.setExecStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
update.setExecStartTime(now);
|
||||
break;
|
||||
case FINISH:
|
||||
case TERMINATED:
|
||||
case FAILURE:
|
||||
update.setExecEndTime(now);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
applicationPipelineTaskDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
log.info("应用流水线执行停止 id: {}", taskId);
|
||||
this.terminated = true;
|
||||
if (currentHandler != null) {
|
||||
currentHandler.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateDetail(Long id) {
|
||||
IStageHandler stageHandler = stageHandlers.get(id);
|
||||
if (stageHandler != null) {
|
||||
stageHandler.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipDetail(Long id) {
|
||||
IStageHandler stageHandler = stageHandlers.get(id);
|
||||
if (stageHandler != null) {
|
||||
stageHandler.skip();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class PipelineSessionHolder {
|
||||
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
private final ConcurrentHashMap<Long, IPipelineProcessor> session = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加 session
|
||||
*
|
||||
* @param processor processor
|
||||
*/
|
||||
public void addSession(IPipelineProcessor processor) {
|
||||
session.put(processor.getTaskId(), processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public IPipelineProcessor getSession(Long id) {
|
||||
return session.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 session
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
public void removeSession(Long id) {
|
||||
session.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline.stage;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.app.PipelineDetailStatus;
|
||||
import cn.orionsec.ops.constant.app.PipelineLogStatus;
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.constant.user.RoleType;
|
||||
import cn.orionsec.ops.dao.ApplicationPipelineTaskDetailDAO;
|
||||
import cn.orionsec.ops.dao.ApplicationPipelineTaskLogDAO;
|
||||
import cn.orionsec.ops.dao.UserInfoDAO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskLogDO;
|
||||
import cn.orionsec.ops.entity.domain.UserInfoDO;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.utils.UserHolder;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractStageHandler implements IStageHandler {
|
||||
|
||||
protected static final ApplicationPipelineTaskDetailDAO applicationPipelineTaskDetailDAO = SpringHolder.getBean(ApplicationPipelineTaskDetailDAO.class);
|
||||
|
||||
protected static final ApplicationPipelineTaskLogDAO applicationPipelineTaskLogDAO = SpringHolder.getBean(ApplicationPipelineTaskLogDAO.class);
|
||||
|
||||
protected static final UserInfoDAO userInfoDAO = SpringHolder.getBean(UserInfoDAO.class);
|
||||
|
||||
protected Long detailId;
|
||||
|
||||
protected ApplicationPipelineTaskDO task;
|
||||
|
||||
protected ApplicationPipelineTaskDetailDO detail;
|
||||
|
||||
protected StageType stageType;
|
||||
|
||||
@Getter
|
||||
protected volatile PipelineDetailStatus status;
|
||||
|
||||
protected volatile boolean terminated;
|
||||
|
||||
public AbstractStageHandler(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
|
||||
this.task = task;
|
||||
this.detail = detail;
|
||||
this.detailId = detail.getId();
|
||||
this.status = PipelineDetailStatus.WAIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
log.info("流水线阶段操作-开始执行 detailId: {}", detailId);
|
||||
// 状态检查
|
||||
if (!this.checkCanRunnable()) {
|
||||
return;
|
||||
}
|
||||
Exception ex = null;
|
||||
// 执行
|
||||
try {
|
||||
// 更新状态
|
||||
this.updateStatus(PipelineDetailStatus.RUNNABLE);
|
||||
// 执行操作
|
||||
this.execStageTask();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 成功回调
|
||||
this.successCallback();
|
||||
} else {
|
||||
// 异常回调
|
||||
this.exceptionCallback(ex);
|
||||
throw Exceptions.runtime(ex.getMessage(), ex);
|
||||
}
|
||||
} finally {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作任务
|
||||
*/
|
||||
protected abstract void execStageTask();
|
||||
|
||||
/**
|
||||
* 检查是否可执行
|
||||
*
|
||||
* @return 是否可执行
|
||||
*/
|
||||
protected boolean checkCanRunnable() {
|
||||
ApplicationPipelineTaskDetailDO detail = applicationPipelineTaskDetailDAO.selectById(detailId);
|
||||
if (detail == null) {
|
||||
return false;
|
||||
}
|
||||
return PipelineDetailStatus.WAIT.getStatus().equals(detail.getExecStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
protected void terminatedCallback() {
|
||||
log.info("流水线阶段操作-终止回调 detailId: {}", detailId);
|
||||
// 修改状态
|
||||
this.updateStatus(PipelineDetailStatus.TERMINATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功回调
|
||||
*/
|
||||
protected void successCallback() {
|
||||
log.info("流水线阶段操作-成功回调 detailId: {}", detailId);
|
||||
// 修改状态
|
||||
this.updateStatus(PipelineDetailStatus.FINISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*
|
||||
* @param ex ex
|
||||
*/
|
||||
protected void exceptionCallback(Exception ex) {
|
||||
log.error("流水线阶段操作-异常回调 detailId: {}", detailId, ex);
|
||||
// 修改状态
|
||||
this.updateStatus(PipelineDetailStatus.FAILURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status 状态
|
||||
*/
|
||||
protected void updateStatus(PipelineDetailStatus status) {
|
||||
this.status = status;
|
||||
Date now = new Date();
|
||||
ApplicationPipelineTaskDetailDO update = new ApplicationPipelineTaskDetailDO();
|
||||
update.setId(detailId);
|
||||
update.setExecStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
update.setExecStartTime(now);
|
||||
break;
|
||||
case FINISH:
|
||||
case FAILURE:
|
||||
case TERMINATED:
|
||||
update.setExecEndTime(now);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// 更新
|
||||
applicationPipelineTaskDetailDAO.updateById(update);
|
||||
// 插入日志
|
||||
String appName = detail.getAppName();
|
||||
switch (status) {
|
||||
case FINISH:
|
||||
this.addLog(PipelineLogStatus.SUCCESS, appName);
|
||||
break;
|
||||
case FAILURE:
|
||||
this.addLog(PipelineLogStatus.FAILURE, appName);
|
||||
break;
|
||||
case SKIPPED:
|
||||
this.addLog(PipelineLogStatus.SKIP, appName);
|
||||
break;
|
||||
case TERMINATED:
|
||||
this.addLog(PipelineLogStatus.TERMINATED, appName);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
log.info("流水线阶段操作-终止 detailId: {}", detailId);
|
||||
this.terminated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip() {
|
||||
log.info("流水线阶段操作-跳过 detailId: {}", detailId);
|
||||
if (PipelineDetailStatus.WAIT.equals(status)) {
|
||||
// 只能跳过等待中的任务
|
||||
this.updateStatus(PipelineDetailStatus.SKIPPED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置引用id
|
||||
*
|
||||
* @param relId relId
|
||||
*/
|
||||
protected void setRelId(Long relId) {
|
||||
ApplicationPipelineTaskDetailDO update = new ApplicationPipelineTaskDetailDO();
|
||||
update.setId(detailId);
|
||||
update.setRelId(relId);
|
||||
update.setUpdateTime(new Date());
|
||||
applicationPipelineTaskDetailDAO.updateById(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加日志
|
||||
*
|
||||
* @param logStatus logStatus
|
||||
* @param params 日志参数
|
||||
*/
|
||||
protected void addLog(PipelineLogStatus logStatus, Object... params) {
|
||||
ApplicationPipelineTaskLogDO log = new ApplicationPipelineTaskLogDO();
|
||||
log.setTaskId(task.getId());
|
||||
log.setTaskDetailId(detail.getId());
|
||||
log.setLogStatus(logStatus.getStatus());
|
||||
log.setStageType(stageType.getType());
|
||||
log.setLogInfo(logStatus.format(stageType, params));
|
||||
applicationPipelineTaskLogDAO.insert(log);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置执行人用户上下文
|
||||
*/
|
||||
protected void setExecuteUserContext() {
|
||||
UserInfoDO userInfo = userInfoDAO.selectById(task.getExecUserId());
|
||||
UserDTO user = new UserDTO();
|
||||
user.setId(task.getExecUserId());
|
||||
user.setUsername(task.getExecUserName());
|
||||
Integer roleType = Optional.ofNullable(userInfo)
|
||||
.map(UserInfoDO::getRoleType)
|
||||
.orElseGet(() -> task.getExecUserId().equals(task.getAuditUserId())
|
||||
? RoleType.ADMINISTRATOR.getType()
|
||||
: RoleType.DEVELOPER.getType());
|
||||
user.setRoleType(roleType);
|
||||
UserHolder.set(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
protected void close() {
|
||||
UserHolder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline.stage;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.app.BuildStatus;
|
||||
import cn.orionsec.ops.constant.app.PipelineLogStatus;
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.dao.ApplicationBuildDAO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationBuildDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
|
||||
import cn.orionsec.ops.entity.dto.app.ApplicationPipelineStageConfigDTO;
|
||||
import cn.orionsec.ops.entity.request.app.ApplicationBuildRequest;
|
||||
import cn.orionsec.ops.handler.app.build.BuildSessionHolder;
|
||||
import cn.orionsec.ops.handler.app.machine.BuildMachineProcessor;
|
||||
import cn.orionsec.ops.handler.app.machine.IMachineProcessor;
|
||||
import cn.orionsec.ops.service.api.ApplicationBuildService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class BuildStageHandler extends AbstractStageHandler {
|
||||
|
||||
private Long buildId;
|
||||
|
||||
private static final ApplicationBuildService applicationBuildService = SpringHolder.getBean(ApplicationBuildService.class);
|
||||
|
||||
private static final ApplicationBuildDAO applicationBuildDAO = SpringHolder.getBean(ApplicationBuildDAO.class);
|
||||
|
||||
private static final BuildSessionHolder buildSessionHolder = SpringHolder.getBean(BuildSessionHolder.class);
|
||||
|
||||
public BuildStageHandler(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
|
||||
super(task, detail);
|
||||
this.stageType = StageType.BUILD;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execStageTask() {
|
||||
// 设置用户上下文
|
||||
this.setExecuteUserContext();
|
||||
// 参数
|
||||
ApplicationBuildRequest request = new ApplicationBuildRequest();
|
||||
request.setAppId(detail.getAppId());
|
||||
request.setProfileId(task.getProfileId());
|
||||
// 配置
|
||||
ApplicationPipelineStageConfigDTO config = JSON.parseObject(detail.getStageConfig(), ApplicationPipelineStageConfigDTO.class);
|
||||
request.setBranchName(config.getBranchName());
|
||||
request.setCommitId(config.getCommitId());
|
||||
request.setDescription(config.getDescription());
|
||||
log.info("执行流水线任务-构建阶段-开始创建 detailId: {}, 参数: {}", detailId, JSON.toJSONString(request));
|
||||
// 创建构建任务
|
||||
this.buildId = applicationBuildService.submitBuildTask(request, false);
|
||||
// 设置构建id
|
||||
this.setRelId(buildId);
|
||||
// 插入创建日志
|
||||
ApplicationBuildDO build = applicationBuildDAO.selectById(buildId);
|
||||
this.addLog(PipelineLogStatus.CREATE, detail.getAppName(), build.getBuildSeq());
|
||||
log.info("执行流水线任务-构建阶段-创建完成开始执行 detailId: {}, buildId: {}", detailId, buildId);
|
||||
// 插入执行日志
|
||||
this.addLog(PipelineLogStatus.EXEC, detail.getAppName());
|
||||
// 执行构建任务
|
||||
new BuildMachineProcessor(buildId).run();
|
||||
// 检查执行结果
|
||||
build = applicationBuildDAO.selectById(buildId);
|
||||
if (BuildStatus.FAILURE.getStatus().equals(build.getBuildStatus())) {
|
||||
// 异常抛出
|
||||
throw Exceptions.runtime(MessageConst.OPERATOR_ERROR);
|
||||
} else if (BuildStatus.TERMINATED.getStatus().equals(build.getBuildStatus())) {
|
||||
this.terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
super.terminate();
|
||||
// 获取数据
|
||||
ApplicationBuildDO build = applicationBuildDAO.selectById(buildId);
|
||||
// 检查状态
|
||||
if (!BuildStatus.RUNNABLE.getStatus().equals(build.getBuildStatus())) {
|
||||
return;
|
||||
}
|
||||
// 获取实例
|
||||
IMachineProcessor session = buildSessionHolder.getSession(buildId);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
// 调用终止
|
||||
session.terminate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline.stage;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.ops.constant.app.PipelineDetailStatus;
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationPipelineTaskDetailDO;
|
||||
|
||||
public interface IStageHandler extends Executable {
|
||||
|
||||
/**
|
||||
* 停止执行
|
||||
*/
|
||||
void terminate();
|
||||
|
||||
/**
|
||||
* 跳过执行
|
||||
*/
|
||||
void skip();
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*
|
||||
* @return status
|
||||
*/
|
||||
PipelineDetailStatus getStatus();
|
||||
|
||||
/**
|
||||
* 获取阶段处理器
|
||||
*
|
||||
* @param task task
|
||||
* @param detail detail
|
||||
* @return 阶段处理器
|
||||
*/
|
||||
static IStageHandler with(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
|
||||
StageType stageType = StageType.of(detail.getStageType());
|
||||
switch (stageType) {
|
||||
case BUILD:
|
||||
return new BuildStageHandler(task, detail);
|
||||
case RELEASE:
|
||||
return new ReleaseStageHandler(task, detail);
|
||||
default:
|
||||
throw Exceptions.argument();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.pipeline.stage;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.CnConst;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.app.PipelineLogStatus;
|
||||
import cn.orionsec.ops.constant.app.ReleaseStatus;
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.dao.ApplicationBuildDAO;
|
||||
import cn.orionsec.ops.dao.ApplicationReleaseDAO;
|
||||
import cn.orionsec.ops.entity.domain.*;
|
||||
import cn.orionsec.ops.entity.dto.app.ApplicationPipelineStageConfigDTO;
|
||||
import cn.orionsec.ops.entity.request.app.ApplicationReleaseRequest;
|
||||
import cn.orionsec.ops.handler.app.release.IReleaseProcessor;
|
||||
import cn.orionsec.ops.handler.app.release.ReleaseSessionHolder;
|
||||
import cn.orionsec.ops.service.api.ApplicationMachineService;
|
||||
import cn.orionsec.ops.service.api.ApplicationReleaseService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class ReleaseStageHandler extends AbstractStageHandler {
|
||||
|
||||
private static final ApplicationBuildDAO applicationBuildDAO = SpringHolder.getBean(ApplicationBuildDAO.class);
|
||||
|
||||
private static final ApplicationReleaseDAO applicationReleaseDAO = SpringHolder.getBean(ApplicationReleaseDAO.class);
|
||||
|
||||
private static final ApplicationReleaseService applicationReleaseService = SpringHolder.getBean(ApplicationReleaseService.class);
|
||||
|
||||
private static final ApplicationMachineService applicationMachineService = SpringHolder.getBean(ApplicationMachineService.class);
|
||||
|
||||
private static final ReleaseSessionHolder releaseSessionHolder = SpringHolder.getBean(ReleaseSessionHolder.class);
|
||||
|
||||
private Long releaseId;
|
||||
|
||||
private ApplicationReleaseDO release;
|
||||
|
||||
public ReleaseStageHandler(ApplicationPipelineTaskDO task, ApplicationPipelineTaskDetailDO detail) {
|
||||
super(task, detail);
|
||||
this.stageType = StageType.RELEASE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execStageTask() {
|
||||
// 创建
|
||||
this.createReleaseTask();
|
||||
// 审核
|
||||
this.auditReleaseTask();
|
||||
// 执行
|
||||
this.execReleaseTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建发布任务
|
||||
*/
|
||||
protected void createReleaseTask() {
|
||||
// 设置用户上下文
|
||||
this.setExecuteUserContext();
|
||||
Long profileId = task.getProfileId();
|
||||
Long appId = detail.getAppId();
|
||||
// 创建发布任务参数
|
||||
ApplicationReleaseRequest request = new ApplicationReleaseRequest();
|
||||
request.setProfileId(profileId);
|
||||
request.setAppId(appId);
|
||||
// 其他配置
|
||||
ApplicationPipelineStageConfigDTO config = JSON.parseObject(detail.getStageConfig(), ApplicationPipelineStageConfigDTO.class);
|
||||
request.setDescription(config.getDescription());
|
||||
// 标题
|
||||
request.setTitle(Strings.def(config.getTitle(), () -> CnConst.RELEASE + Const.SPACE + detail.getAppName()));
|
||||
// 构建id
|
||||
Long buildId = config.getBuildId();
|
||||
ApplicationBuildDO build = null;
|
||||
if (buildId == null) {
|
||||
// 最新版本
|
||||
List<ApplicationBuildDO> buildList = applicationBuildDAO.selectBuildReleaseList(appId, profileId, 1);
|
||||
if (!buildList.isEmpty()) {
|
||||
build = buildList.get(0);
|
||||
buildId = build.getId();
|
||||
}
|
||||
} else {
|
||||
build = applicationBuildDAO.selectById(buildId);
|
||||
}
|
||||
if (build == null) {
|
||||
throw Exceptions.argument(Strings.format(MessageConst.APP_LAST_BUILD_VERSION_ABSENT, detail.getAppName()));
|
||||
}
|
||||
request.setBuildId(buildId);
|
||||
config.setBuildId(buildId);
|
||||
// 发布机器
|
||||
List<Long> machineIdList = config.getMachineIdList();
|
||||
if (Lists.isEmpty(machineIdList)) {
|
||||
// 全部机器
|
||||
machineIdList = applicationMachineService.getAppProfileMachineList(appId, profileId).stream()
|
||||
.map(ApplicationMachineDO::getMachineId)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
request.setMachineIdList(machineIdList);
|
||||
config.setMachineIdList(machineIdList);
|
||||
// 更新详情配置
|
||||
String configJson = JSON.toJSONString(config);
|
||||
detail.setStageConfig(configJson);
|
||||
ApplicationPipelineTaskDetailDO updateConfig = new ApplicationPipelineTaskDetailDO();
|
||||
updateConfig.setId(detailId);
|
||||
updateConfig.setStageConfig(configJson);
|
||||
applicationPipelineTaskDetailDAO.updateById(updateConfig);
|
||||
// 创建发布任务
|
||||
log.info("执行流水线任务-发布阶段-开始创建 detailId: {}, 参数: {}", detailId, JSON.toJSONString(request));
|
||||
this.releaseId = applicationReleaseService.submitAppRelease(request);
|
||||
// 设置发布id
|
||||
this.setRelId(releaseId);
|
||||
// 插入日志
|
||||
this.addLog(PipelineLogStatus.CREATE, detail.getAppName(), build.getBuildSeq());
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核发布任务
|
||||
*/
|
||||
private void auditReleaseTask() {
|
||||
// 查询发布任务
|
||||
this.release = applicationReleaseDAO.selectById(releaseId);
|
||||
if (!ReleaseStatus.WAIT_AUDIT.getStatus().equals(release.getReleaseStatus())) {
|
||||
return;
|
||||
}
|
||||
ApplicationReleaseDO update = new ApplicationReleaseDO();
|
||||
update.setId(releaseId);
|
||||
update.setAuditUserId(task.getAuditUserId());
|
||||
update.setAuditUserName(task.getAuditUserName());
|
||||
update.setAuditReason(MessageConst.AUTO_AUDIT_RESOLVE);
|
||||
update.setAuditTime(new Date());
|
||||
log.info("执行流水线任务-发布阶段-审核 detailId: {}, releaseId: {}, 参数: {}", detailId, releaseId, JSON.toJSONString(update));
|
||||
applicationReleaseDAO.updateById(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行发布任务
|
||||
*/
|
||||
private void execReleaseTask() {
|
||||
log.info("执行流水线任务-发布阶段-开始执行 detailId: {}, releaseId: {}", detailId, releaseId);
|
||||
// 提交发布任务
|
||||
applicationReleaseService.runnableAppRelease(releaseId, true, false);
|
||||
// 插入执行日志
|
||||
this.addLog(PipelineLogStatus.EXEC, detail.getAppName());
|
||||
// 执行发布任务
|
||||
IReleaseProcessor.with(release).run();
|
||||
// 检查执行结果
|
||||
this.release = applicationReleaseDAO.selectById(releaseId);
|
||||
if (ReleaseStatus.FAILURE.getStatus().equals(release.getReleaseStatus())) {
|
||||
// 异常抛出
|
||||
throw Exceptions.runtime(MessageConst.OPERATOR_ERROR);
|
||||
} else if (ReleaseStatus.TERMINATED.getStatus().equals(release.getReleaseStatus())) {
|
||||
// 停止
|
||||
this.terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
super.terminate();
|
||||
// 获取数据
|
||||
this.release = applicationReleaseDAO.selectById(releaseId);
|
||||
// 检查状态
|
||||
if (!ReleaseStatus.RUNNABLE.getStatus().equals(release.getReleaseStatus())) {
|
||||
return;
|
||||
}
|
||||
// 获取实例
|
||||
IReleaseProcessor session = releaseSessionHolder.getSession(releaseId);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
// 调用终止
|
||||
session.terminateAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.release;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.app.ReleaseStatus;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.message.MessageType;
|
||||
import cn.orionsec.ops.dao.ApplicationReleaseDAO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationReleaseDO;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationReleaseMachineDO;
|
||||
import cn.orionsec.ops.handler.app.machine.ReleaseMachineProcessor;
|
||||
import cn.orionsec.ops.service.api.ApplicationReleaseMachineService;
|
||||
import cn.orionsec.ops.service.api.WebSideMessageService;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractReleaseProcessor implements IReleaseProcessor {
|
||||
|
||||
protected static final ApplicationReleaseDAO applicationReleaseDAO = SpringHolder.getBean(ApplicationReleaseDAO.class);
|
||||
|
||||
protected static final ApplicationReleaseMachineService applicationReleaseMachineService = SpringHolder.getBean(ApplicationReleaseMachineService.class);
|
||||
|
||||
protected static final ReleaseSessionHolder releaseSessionHolder = SpringHolder.getBean(ReleaseSessionHolder.class);
|
||||
|
||||
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
|
||||
|
||||
@Getter
|
||||
private final Long releaseId;
|
||||
|
||||
protected ApplicationReleaseDO release;
|
||||
|
||||
protected Map<Long, ReleaseMachineProcessor> machineProcessors;
|
||||
|
||||
protected volatile boolean terminated;
|
||||
|
||||
public AbstractReleaseProcessor(Long releaseId) {
|
||||
this.releaseId = releaseId;
|
||||
this.machineProcessors = Maps.newLinkedMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
log.info("已提交应用发布执行任务 id: {}", releaseId);
|
||||
Threads.start(this, SchedulerPools.RELEASE_MAIN_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("应用发布任务执行开始 id: {}", releaseId);
|
||||
// 执行
|
||||
Exception ex = null;
|
||||
try {
|
||||
// 查询数据
|
||||
this.getReleaseData();
|
||||
// 检查状态
|
||||
if (release != null && !ReleaseStatus.WAIT_RUNNABLE.getStatus().equals(release.getReleaseStatus())
|
||||
&& !ReleaseStatus.WAIT_SCHEDULE.getStatus().equals(release.getReleaseStatus())) {
|
||||
return;
|
||||
}
|
||||
// 修改状态
|
||||
this.updateStatus(ReleaseStatus.RUNNABLE);
|
||||
// 添加会话
|
||||
releaseSessionHolder.addSession(this);
|
||||
// 执行
|
||||
this.handler();
|
||||
} catch (Exception e) {
|
||||
log.error("应用发布任务执行初始化失败 id: {}, {}", releaseId, e);
|
||||
ex = e;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 成功回调
|
||||
this.completeCallback();
|
||||
} else {
|
||||
// 异常回调
|
||||
this.exceptionCallback(ex);
|
||||
}
|
||||
} finally {
|
||||
// 释放资源
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理器
|
||||
*
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
protected abstract void handler() throws Exception;
|
||||
|
||||
@Override
|
||||
public void terminateAll() {
|
||||
this.terminated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateMachine(Long releaseMachineId) {
|
||||
ReleaseMachineProcessor processor = machineProcessors.get(releaseMachineId);
|
||||
if (processor != null) {
|
||||
processor.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipMachine(Long releaseMachineId) {
|
||||
ReleaseMachineProcessor processor = machineProcessors.get(releaseMachineId);
|
||||
if (processor != null) {
|
||||
processor.skip();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMachine(Long releaseMachineId, String command) {
|
||||
ReleaseMachineProcessor processor = machineProcessors.get(releaseMachineId);
|
||||
if (processor != null) {
|
||||
processor.write(command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发布数据
|
||||
*/
|
||||
protected void getReleaseData() {
|
||||
// 查询发布信息主表
|
||||
this.release = applicationReleaseDAO.selectById(releaseId);
|
||||
if (release == null) {
|
||||
return;
|
||||
}
|
||||
if (!ReleaseStatus.WAIT_RUNNABLE.getStatus().equals(release.getReleaseStatus())
|
||||
&& !ReleaseStatus.WAIT_SCHEDULE.getStatus().equals(release.getReleaseStatus())) {
|
||||
return;
|
||||
}
|
||||
// 查询发布机器
|
||||
List<ApplicationReleaseMachineDO> machines = applicationReleaseMachineService.getReleaseMachines(releaseId);
|
||||
for (ApplicationReleaseMachineDO machine : machines) {
|
||||
machineProcessors.put(machine.getId(), new ReleaseMachineProcessor(release, machine));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
protected void terminatedCallback() {
|
||||
log.info("应用发布任务执行执行停止 id: {}", releaseId);
|
||||
this.updateStatus(ReleaseStatus.TERMINATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成回调
|
||||
*/
|
||||
protected void completeCallback() {
|
||||
log.info("应用发布任务执行执行完成 id: {}", releaseId);
|
||||
this.updateStatus(ReleaseStatus.FINISH);
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, release.getId());
|
||||
params.put(EventKeys.TITLE, release.getReleaseTitle());
|
||||
webSideMessageService.addMessage(MessageType.RELEASE_SUCCESS, release.getId(), release.getReleaseUserId(), release.getReleaseUserName(), params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*
|
||||
* @param ex ex
|
||||
*/
|
||||
protected void exceptionCallback(Exception ex) {
|
||||
log.error("应用发布任务执行执行失败 id: {}", releaseId, ex);
|
||||
this.updateStatus(ReleaseStatus.FAILURE);
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, release.getId());
|
||||
params.put(EventKeys.TITLE, release.getReleaseTitle());
|
||||
webSideMessageService.addMessage(MessageType.RELEASE_FAILURE, release.getId(), release.getReleaseUserId(), release.getReleaseUserName(), params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
protected void updateStatus(ReleaseStatus status) {
|
||||
Date now = new Date();
|
||||
ApplicationReleaseDO update = new ApplicationReleaseDO();
|
||||
update.setId(releaseId);
|
||||
update.setReleaseStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
update.setReleaseStartTime(now);
|
||||
break;
|
||||
case FINISH:
|
||||
case TERMINATED:
|
||||
case FAILURE:
|
||||
update.setReleaseEndTime(now);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
applicationReleaseDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
releaseSessionHolder.removeSession(releaseId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.release;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.kit.lang.function.select.Branches;
|
||||
import cn.orionsec.kit.lang.function.select.Selector;
|
||||
import cn.orionsec.ops.constant.common.SerialType;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationReleaseDO;
|
||||
|
||||
public interface IReleaseProcessor extends Executable, Runnable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 获取id
|
||||
*
|
||||
* @return id
|
||||
*/
|
||||
Long getReleaseId();
|
||||
|
||||
/**
|
||||
* 终止
|
||||
*/
|
||||
void terminateAll();
|
||||
|
||||
/**
|
||||
* 终止机器操作
|
||||
*
|
||||
* @param releaseMachineId releaseMachineId
|
||||
*/
|
||||
void terminateMachine(Long releaseMachineId);
|
||||
|
||||
/**
|
||||
* 跳过机器操作
|
||||
*
|
||||
* @param releaseMachineId 机器id
|
||||
*/
|
||||
void skipMachine(Long releaseMachineId);
|
||||
|
||||
/**
|
||||
* 输入机器命令
|
||||
*
|
||||
* @param releaseMachineId 机器id
|
||||
* @param command command
|
||||
*/
|
||||
void writeMachine(Long releaseMachineId, String command);
|
||||
|
||||
/**
|
||||
* 获取发布执行器
|
||||
*
|
||||
* @param release release
|
||||
* @return 执行器
|
||||
*/
|
||||
static IReleaseProcessor with(ApplicationReleaseDO release) {
|
||||
return Selector.<SerialType, IReleaseProcessor>of(SerialType.of(release.getReleaseSerialize()))
|
||||
.test(Branches.eq(SerialType.SERIAL)
|
||||
.then(() -> new SerialReleaseProcessor(release.getId())))
|
||||
.test(Branches.eq(SerialType.PARALLEL)
|
||||
.then(() -> new ParallelReleaseProcessor(release.getId())))
|
||||
.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.release;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.app.ActionStatus;
|
||||
import cn.orionsec.ops.handler.app.machine.ReleaseMachineProcessor;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class ParallelReleaseProcessor extends AbstractReleaseProcessor {
|
||||
|
||||
public ParallelReleaseProcessor(Long releaseId) {
|
||||
super(releaseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
Collection<ReleaseMachineProcessor> processor = machineProcessors.values();
|
||||
Threads.blockRun(processor, SchedulerPools.RELEASE_MACHINE_SCHEDULER);
|
||||
// 检查是否停止
|
||||
if (terminated) {
|
||||
return;
|
||||
}
|
||||
// 全部停止
|
||||
final boolean allTerminated = processor.stream()
|
||||
.map(ReleaseMachineProcessor::getStatus)
|
||||
.allMatch(ActionStatus.TERMINATED::equals);
|
||||
if (allTerminated) {
|
||||
this.terminated = true;
|
||||
return;
|
||||
}
|
||||
// 全部完成
|
||||
boolean allFinish = processor.stream()
|
||||
.map(ReleaseMachineProcessor::getStatus)
|
||||
.filter(s -> !ActionStatus.TERMINATED.equals(s))
|
||||
.allMatch(ActionStatus.FINISH::equals);
|
||||
if (!allFinish) {
|
||||
throw Exceptions.log(MessageConst.OPERATOR_NOT_ALL_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAll() {
|
||||
super.terminateAll();
|
||||
machineProcessors.values().forEach(ReleaseMachineProcessor::terminate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.release;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class ReleaseSessionHolder {
|
||||
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
private final ConcurrentHashMap<Long, IReleaseProcessor> session = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加 session
|
||||
*
|
||||
* @param processor processor
|
||||
*/
|
||||
public void addSession(IReleaseProcessor processor) {
|
||||
session.put(processor.getReleaseId(), processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public IReleaseProcessor getSession(Long id) {
|
||||
return session.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 session
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
public void removeSession(Long id) {
|
||||
session.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
package cn.orionsec.ops.handler.app.release;
|
||||
|
||||
import cn.orionsec.ops.constant.app.ActionStatus;
|
||||
import cn.orionsec.ops.constant.common.ExceptionHandlerType;
|
||||
import cn.orionsec.ops.handler.app.machine.IMachineProcessor;
|
||||
import cn.orionsec.ops.handler.app.machine.ReleaseMachineProcessor;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SerialReleaseProcessor extends AbstractReleaseProcessor {
|
||||
|
||||
public SerialReleaseProcessor(Long releaseId) {
|
||||
super(releaseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
// 异常处理策略
|
||||
final boolean errorSkipAll = ExceptionHandlerType.SKIP_ALL.getType().equals(release.getExceptionHandler());
|
||||
Exception ex = null;
|
||||
Collection<ReleaseMachineProcessor> processors = machineProcessors.values();
|
||||
for (ReleaseMachineProcessor processor : processors) {
|
||||
// 停止则跳过
|
||||
if (terminated) {
|
||||
processor.skip();
|
||||
continue;
|
||||
}
|
||||
// 发生异常并且异常处理策略是跳过所有则跳过
|
||||
if (ex != null && errorSkipAll) {
|
||||
processor.skip();
|
||||
continue;
|
||||
}
|
||||
// 执行
|
||||
try {
|
||||
processor.run();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
}
|
||||
// 异常返回
|
||||
if (ex != null) {
|
||||
throw ex;
|
||||
}
|
||||
// 全部停止
|
||||
final boolean allTerminated = processors.stream()
|
||||
.map(ReleaseMachineProcessor::getStatus)
|
||||
.filter(s -> !ActionStatus.SKIPPED.equals(s))
|
||||
.allMatch(ActionStatus.TERMINATED::equals);
|
||||
if (allTerminated) {
|
||||
this.terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAll() {
|
||||
super.terminateAll();
|
||||
// 获取当前执行中的机器执行器
|
||||
machineProcessors.values().stream()
|
||||
.filter(s -> s.getStatus().equals(ActionStatus.RUNNABLE))
|
||||
.forEach(IMachineProcessor::terminate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
|
||||
package cn.orionsec.ops.handler.exec;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.Letters;
|
||||
import cn.orionsec.kit.lang.exception.DisabledException;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.ssh.ExitCode;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.command.ExecStatus;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.message.MessageType;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.dao.CommandExecDAO;
|
||||
import cn.orionsec.ops.entity.domain.CommandExecDO;
|
||||
import cn.orionsec.ops.entity.domain.MachineInfoDO;
|
||||
import cn.orionsec.ops.handler.tail.TailSessionHolder;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.service.api.WebSideMessageService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class CommandExecHandler implements IExecHandler {
|
||||
|
||||
private static final CommandExecDAO commandExecDAO = SpringHolder.getBean(CommandExecDAO.class);
|
||||
|
||||
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private static final ExecSessionHolder execSessionHolder = SpringHolder.getBean(ExecSessionHolder.class);
|
||||
|
||||
private static final TailSessionHolder tailSessionHolder = SpringHolder.getBean(TailSessionHolder.class);
|
||||
|
||||
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
|
||||
|
||||
private final Long execId;
|
||||
|
||||
private CommandExecDO record;
|
||||
|
||||
private MachineInfoDO machine;
|
||||
|
||||
private SessionStore sessionStore;
|
||||
|
||||
private CommandExecutor executor;
|
||||
|
||||
private int exitCode;
|
||||
|
||||
private String logPath;
|
||||
|
||||
private OutputStream logOutputStream;
|
||||
|
||||
private Date startTime, endTime;
|
||||
|
||||
private volatile boolean terminated;
|
||||
|
||||
protected CommandExecHandler(Long execId) {
|
||||
this.execId = execId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
log.info("execHandler-提交 execId: {}", execId);
|
||||
Threads.start(this, SchedulerPools.EXEC_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("execHandler-执行开始 execId: {}", execId);
|
||||
// 获取执行数据
|
||||
this.getExecData();
|
||||
// 检查状态
|
||||
if (record == null || !ExecStatus.WAITING.getStatus().equals(record.getExecStatus())) {
|
||||
return;
|
||||
}
|
||||
// 执行
|
||||
Exception ex = null;
|
||||
try {
|
||||
// 更新状态
|
||||
this.updateStatus(ExecStatus.RUNNABLE);
|
||||
execSessionHolder.addSession(execId, this);
|
||||
// 打开日志
|
||||
this.openLogger();
|
||||
// 打开executor
|
||||
this.sessionStore = machineInfoService.openSessionStore(machine);
|
||||
this.executor = sessionStore.getCommandExecutor(Strings.replaceCRLF(record.getExecCommand()));
|
||||
// 执行命令
|
||||
CommandExecutors.execCommand(executor, logOutputStream);
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 完成回调
|
||||
this.completeCallback();
|
||||
} else if (ex instanceof DisabledException) {
|
||||
// 机器未启用回调
|
||||
this.machineDisableCallback();
|
||||
} else {
|
||||
// 执行失败回调
|
||||
this.exceptionCallback(ex);
|
||||
}
|
||||
} finally {
|
||||
// 释放资源
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取执行数据
|
||||
*/
|
||||
private void getExecData() {
|
||||
this.record = commandExecDAO.selectById(execId);
|
||||
if (record == null) {
|
||||
return;
|
||||
}
|
||||
if (!ExecStatus.WAITING.getStatus().equals(record.getExecStatus())) {
|
||||
return;
|
||||
}
|
||||
// 查询机器信息
|
||||
this.machine = machineInfoService.selectById(record.getMachineId());
|
||||
// 设置日志信息
|
||||
File logFile = new File(Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), record.getLogPath()));
|
||||
Files1.touch(logFile);
|
||||
this.logPath = logFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String out) {
|
||||
executor.write(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
log.info("execHandler-停止 execId: {}", execId);
|
||||
this.terminated = true;
|
||||
Streams.close(executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开日志
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void openLogger() {
|
||||
// 打开日志流
|
||||
log.info("execHandler-打开日志流 {} {}", execId, logPath);
|
||||
File logFile = new File(logPath);
|
||||
this.logOutputStream = Files1.openOutputStreamFast(logFile);
|
||||
StringBuilder sb = new StringBuilder()
|
||||
.append(Utils.getStainKeyWords("# 准备执行命令", StainCode.GLOSS_GREEN))
|
||||
.append(Letters.LF)
|
||||
.append("@ssh: ")
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append(machine.getUsername()).append("@")
|
||||
.append(machine.getMachineHost()).append(":")
|
||||
.append(machine.getSshPort())
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Letters.LF);
|
||||
sb.append("执行用户: ")
|
||||
.append(Utils.getStainKeyWords(record.getUserName(), StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF);
|
||||
sb.append("执行任务: ")
|
||||
.append(Utils.getStainKeyWords(execId, StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF);
|
||||
sb.append("执行机器: ")
|
||||
.append(Utils.getStainKeyWords(machine.getMachineName(), StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF);
|
||||
sb.append("开始时间: ")
|
||||
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF);
|
||||
String description = record.getDescription();
|
||||
if (!Strings.isBlank(description)) {
|
||||
sb.append("执行描述: ")
|
||||
.append(Utils.getStainKeyWords(description, StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF);
|
||||
}
|
||||
sb.append(Letters.LF)
|
||||
.append(Utils.getStainKeyWords("# 执行命令", StainCode.GLOSS_GREEN))
|
||||
.append(Letters.LF)
|
||||
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
|
||||
.append(Utils.getEndLfWithEof(record.getExecCommand()))
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Utils.getStainKeyWords("# 开始执行", StainCode.GLOSS_GREEN))
|
||||
.append(Letters.LF);
|
||||
logOutputStream.write(Strings.bytes(sb.toString()));
|
||||
logOutputStream.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
private void terminatedCallback() {
|
||||
log.info("execHandler-执行停止 execId: {}", execId);
|
||||
// 更新状态
|
||||
this.updateStatus(ExecStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF)
|
||||
.append(Utils.getStainKeyWords("# 命令执行停止", StainCode.GLOSS_YELLOW))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成回调
|
||||
*/
|
||||
private void completeCallback() {
|
||||
this.exitCode = executor.getExitCode();
|
||||
log.info("execHandler-执行完成 execId: {} exitCode: {}", execId, exitCode);
|
||||
// 更新状态
|
||||
this.updateStatus(ExecStatus.COMPLETE);
|
||||
// 拼接日志
|
||||
long used = endTime.getTime() - startTime.getTime();
|
||||
StringBuilder sb = new StringBuilder()
|
||||
.append(Letters.LF)
|
||||
.append(Utils.getStainKeyWords("# 命令执行完毕", StainCode.GLOSS_GREEN))
|
||||
.append(Letters.LF);
|
||||
sb.append("exitcode: ")
|
||||
.append(ExitCode.isSuccess(exitCode)
|
||||
? Utils.getStainKeyWords(exitCode, StainCode.GLOSS_BLUE)
|
||||
: Utils.getStainKeyWords(exitCode, StainCode.GLOSS_RED))
|
||||
.append(Letters.LF);
|
||||
sb.append("结束时间: ")
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(" used ")
|
||||
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
|
||||
.append(" (")
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append(used)
|
||||
.append("ms")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(")\n");
|
||||
this.appendLog(sb.toString());
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, record.getId());
|
||||
params.put(EventKeys.NAME, record.getMachineName());
|
||||
webSideMessageService.addMessage(MessageType.EXEC_SUCCESS, record.getId(), record.getUserId(), record.getUserName(), params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器未启用回调
|
||||
*/
|
||||
private void machineDisableCallback() {
|
||||
log.info("execHandler-机器停用停止 execId: {}", execId);
|
||||
// 更新状态
|
||||
this.updateStatus(ExecStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Const.LF)
|
||||
.append(Utils.getStainKeyWords("# 命令执行机器未启用", StainCode.GLOSS_YELLOW))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*
|
||||
* @param e e
|
||||
*/
|
||||
private void exceptionCallback(Exception e) {
|
||||
log.error("execHandler-执行失败 execId: {}", execId, e);
|
||||
// 更新状态
|
||||
this.updateStatus(ExecStatus.EXCEPTION);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Const.LF)
|
||||
.append(Utils.getStainKeyWords("# 命令执行异常", StainCode.GLOSS_RED))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF)
|
||||
.append(Exceptions.getStackTraceAsString(e))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
// 发送站内信
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.ID, record.getId());
|
||||
params.put(EventKeys.NAME, record.getMachineName());
|
||||
webSideMessageService.addMessage(MessageType.EXEC_FAILURE, record.getId(), record.getUserId(), record.getUserName(), params);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void appendLog(String log) {
|
||||
logOutputStream.write(Strings.bytes(log));
|
||||
logOutputStream.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
private void updateStatus(ExecStatus status) {
|
||||
Date now = new Date();
|
||||
// 更新
|
||||
CommandExecDO update = new CommandExecDO();
|
||||
update.setId(execId);
|
||||
update.setExecStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
this.startTime = now;
|
||||
update.setStartDate(now);
|
||||
break;
|
||||
case COMPLETE:
|
||||
this.endTime = now;
|
||||
update.setEndDate(now);
|
||||
update.setExitCode(exitCode);
|
||||
break;
|
||||
case EXCEPTION:
|
||||
case TERMINATED:
|
||||
this.endTime = now;
|
||||
update.setEndDate(now);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
int effect = commandExecDAO.updateById(update);
|
||||
log.info("execHandler-更新状态 id: {}, status: {}, effect: {}", execId, status, effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
log.info("execHandler-关闭 id: {}", execId);
|
||||
// 移除会话
|
||||
execSessionHolder.removeSession(execId);
|
||||
// 释放资源
|
||||
Streams.close(executor);
|
||||
Streams.close(sessionStore);
|
||||
Streams.close(logOutputStream);
|
||||
// 异步关闭正在tail的日志
|
||||
tailSessionHolder.asyncCloseTailFile(Const.HOST_MACHINE_ID, logPath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
package cn.orionsec.ops.handler.exec;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class ExecSessionHolder {
|
||||
|
||||
/**
|
||||
* key: execId
|
||||
* value: IExecHandler
|
||||
*/
|
||||
private final Map<Long, IExecHandler> holder = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加session
|
||||
*
|
||||
* @param id id
|
||||
* @param session session
|
||||
*/
|
||||
public void addSession(Long id, IExecHandler session) {
|
||||
holder.put(id, session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public IExecHandler getSession(Long id) {
|
||||
return holder.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public IExecHandler removeSession(Long id) {
|
||||
return holder.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
package cn.orionsec.ops.handler.exec;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
|
||||
public interface IExecHandler extends Runnable, Executable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 写入
|
||||
*
|
||||
* @param out out
|
||||
*/
|
||||
void write(String out);
|
||||
|
||||
/**
|
||||
* 停止
|
||||
*/
|
||||
void terminate();
|
||||
|
||||
/**
|
||||
* 获取实际执行 handler
|
||||
*
|
||||
* @param execId execId
|
||||
* @return handler
|
||||
*/
|
||||
static IExecHandler with(Long execId) {
|
||||
return new CommandExecHandler(execId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
package cn.orionsec.ops.handler.http;
|
||||
|
||||
import cn.orionsec.kit.http.support.HttpMethod;
|
||||
|
||||
public interface HttpApiDefined {
|
||||
|
||||
/**
|
||||
* 请求路径
|
||||
*
|
||||
* @return 路径
|
||||
*/
|
||||
String getPath();
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*
|
||||
* @return 方法
|
||||
*/
|
||||
HttpMethod getMethod();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
package cn.orionsec.ops.handler.http;
|
||||
|
||||
import cn.orionsec.kit.http.ok.OkRequest;
|
||||
import cn.orionsec.kit.lang.constant.StandardContentType;
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
|
||||
public class HttpApiRequest extends OkRequest {
|
||||
|
||||
public HttpApiRequest(String url, HttpApiDefined api) {
|
||||
this.url = url + api.getPath();
|
||||
this.method = api.getMethod().method();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 json body
|
||||
*
|
||||
* @param body body
|
||||
* @return this
|
||||
*/
|
||||
public HttpApiRequest jsonBody(Object body) {
|
||||
this.contentType = StandardContentType.APPLICATION_JSON;
|
||||
this.body(JSON.toJSONString(body));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求获取 httpWrapper
|
||||
*
|
||||
* @param dataClass T
|
||||
* @param <T> T
|
||||
* @return httpWrapper
|
||||
*/
|
||||
public <T> HttpWrapper<T> getHttpWrapper(Class<T> dataClass) {
|
||||
return this.await().getJsonObjectBody(new TypeReference<HttpWrapper<T>>(dataClass) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求获取 json
|
||||
*
|
||||
* @param type type
|
||||
* @param <T> T
|
||||
* @return T
|
||||
*/
|
||||
public <T> T getJson(TypeReference<T> type) {
|
||||
return this.await().getJsonObjectBody(type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
package cn.orionsec.ops.handler.http;
|
||||
|
||||
import cn.orionsec.kit.http.ok.OkResponse;
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import com.alibaba.fastjson.TypeReference;
|
||||
|
||||
public interface HttpApiRequester<API extends HttpApiDefined> {
|
||||
|
||||
/**
|
||||
* 请求 api
|
||||
*
|
||||
* @return OkResponse
|
||||
*/
|
||||
default OkResponse await() {
|
||||
return this.getRequest().await();
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 api
|
||||
*
|
||||
* @param dataClass dataClass
|
||||
* @param <T> T
|
||||
* @return HttpWrapper
|
||||
*/
|
||||
default <T> HttpWrapper<T> request(Class<T> dataClass) {
|
||||
return this.getRequest().getHttpWrapper(dataClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 api
|
||||
*
|
||||
* @param type type
|
||||
* @param <T> T
|
||||
* @return T
|
||||
*/
|
||||
default <T> T request(TypeReference<T> type) {
|
||||
return this.getRequest().getJson(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 api
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param dataClass dataClass
|
||||
* @param <T> T
|
||||
* @return HttpWrapper
|
||||
*/
|
||||
default <T> HttpWrapper<T> request(Object requestBody, Class<T> dataClass) {
|
||||
return this.getRequest().jsonBody(requestBody).getHttpWrapper(dataClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 api
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* @param type type
|
||||
* @param <T> T
|
||||
* @return T
|
||||
*/
|
||||
default <T> T request(Object requestBody, TypeReference<T> type) {
|
||||
return this.getRequest().jsonBody(requestBody).getJson(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 api request
|
||||
*
|
||||
* @return request
|
||||
*/
|
||||
HttpApiRequest getRequest();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
package cn.orionsec.ops.handler.http;
|
||||
|
||||
import cn.orionsec.kit.http.support.HttpMethod;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum MachineMonitorHttpApi implements HttpApiDefined {
|
||||
|
||||
/**
|
||||
* 端点 ping
|
||||
*/
|
||||
ENDPOINT_PING("/orion/machine-monitor-agent/api/endpoint/ping", HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* 端点 version
|
||||
*/
|
||||
ENDPOINT_VERSION("/orion/machine-monitor-agent/api/endpoint/version", HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* 端点 sync
|
||||
*/
|
||||
ENDPOINT_SYNC("/orion/machine-monitor-agent/api/endpoint/sync", HttpMethod.POST),
|
||||
|
||||
/**
|
||||
* 指标 获取机器基本指标
|
||||
*/
|
||||
METRICS_BASE("/orion/machine-monitor-agent/api/metrics/base", HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* 指标 获取系统负载
|
||||
*/
|
||||
METRICS_SYSTEM_LOAD("/orion/machine-monitor-agent/api/metrics/system-load", HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* 指标 获取硬盘名称
|
||||
*/
|
||||
METRICS_DISK_NAME("/orion/machine-monitor-agent/api/metrics/disk-name", HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* 指标 获取 top 进程
|
||||
*/
|
||||
METRICS_TOP_PROCESSES("/orion/machine-monitor-agent/api/metrics/top-processes", HttpMethod.GET),
|
||||
|
||||
/**
|
||||
* 监控 获取cpu数据
|
||||
*/
|
||||
MONITOR_CPU("/orion/machine-monitor-agent/api/monitor-statistic/cpu", HttpMethod.POST),
|
||||
|
||||
/**
|
||||
* 监控 获取内存数据
|
||||
*/
|
||||
MONITOR_MEMORY("/orion/machine-monitor-agent/api/monitor-statistic/memory", HttpMethod.POST),
|
||||
|
||||
/**
|
||||
* 监控 获取网络数据
|
||||
*/
|
||||
MONITOR_NET("/orion/machine-monitor-agent/api/monitor-statistic/net", HttpMethod.POST),
|
||||
|
||||
/**
|
||||
* 监控 获取磁盘数据
|
||||
*/
|
||||
MONITOR_DISK("/orion/machine-monitor-agent/api/monitor-statistic/disk", HttpMethod.POST),
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* 请求路径
|
||||
*/
|
||||
private final String path;
|
||||
|
||||
/**
|
||||
* 请求方法
|
||||
*/
|
||||
private final HttpMethod method;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
package cn.orionsec.ops.handler.http;
|
||||
|
||||
import cn.orionsec.ops.constant.monitor.MonitorConst;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MachineMonitorHttpApiRequester implements HttpApiRequester<MachineMonitorHttpApi> {
|
||||
|
||||
private static final String TAG = "machine-monitor";
|
||||
|
||||
private String url;
|
||||
|
||||
private MachineMonitorHttpApi api;
|
||||
|
||||
private String accessToken;
|
||||
|
||||
@Override
|
||||
public HttpApiRequest getRequest() {
|
||||
HttpApiRequest request = new HttpApiRequest(url, api);
|
||||
request.tag(TAG);
|
||||
request.header(MonitorConst.DEFAULT_ACCESS_HEADER, accessToken);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
|
||||
package cn.orionsec.ops.handler.monitor;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.Letters;
|
||||
import cn.orionsec.kit.lang.utils.Arrays1;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
|
||||
import cn.orionsec.kit.net.host.ssh.ExitCode;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.message.MessageType;
|
||||
import cn.orionsec.ops.constant.monitor.MonitorConst;
|
||||
import cn.orionsec.ops.constant.monitor.MonitorStatus;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.entity.domain.MachineInfoDO;
|
||||
import cn.orionsec.ops.entity.domain.MachineMonitorDO;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.service.api.MachineEnvService;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.service.api.MachineMonitorService;
|
||||
import cn.orionsec.ops.service.api.WebSideMessageService;
|
||||
import cn.orionsec.ops.utils.PathBuilders;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class MonitorAgentInstallTask implements Runnable {
|
||||
|
||||
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private static final MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
|
||||
|
||||
private static final MachineMonitorService machineMonitorService = SpringHolder.getBean(MachineMonitorService.class);
|
||||
|
||||
private static final WebSideMessageService webSideMessageService = SpringHolder.getBean(WebSideMessageService.class);
|
||||
|
||||
private final Long machineId;
|
||||
|
||||
private final UserDTO user;
|
||||
|
||||
private SessionStore session;
|
||||
|
||||
private MachineInfoDO machine;
|
||||
|
||||
private OutputStream logStream;
|
||||
|
||||
public MonitorAgentInstallTask(Long machineId, UserDTO user) {
|
||||
this.machineId = machineId;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("开始安装监控插件 machineId: {}", machineId);
|
||||
try {
|
||||
// 查询机器信息
|
||||
this.machine = machineInfoService.selectById(machineId);
|
||||
// 打开日志流
|
||||
String logPath = PathBuilders.getInstallLogPath(machineId, MonitorConst.AGENT_FILE_NAME_PREFIX);
|
||||
File logFile = new File(Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), logPath));
|
||||
Files1.touch(logFile);
|
||||
this.logStream = Files1.openOutputStreamFast(logFile);
|
||||
// 打开会话
|
||||
this.session = machineInfoService.openSessionStore(machineId);
|
||||
String pluginDirectory = PathBuilders.getPluginPath(machine.getUsername());
|
||||
String startScriptPath = pluginDirectory + "/" + MonitorConst.START_SCRIPT_FILE_NAME;
|
||||
// 传输
|
||||
this.transferAgentFile(pluginDirectory, startScriptPath);
|
||||
// 启动
|
||||
this.startAgentApp(startScriptPath);
|
||||
// 同步等待
|
||||
this.checkAgentRunStatus();
|
||||
// 拼接日志
|
||||
this.appendLog("安装成功 {}", Dates.current());
|
||||
} catch (Exception e) {
|
||||
// 拼接日志
|
||||
this.appendLog("安装失败 {}", Exceptions.getStackTraceAsString(e));
|
||||
// 更新状态
|
||||
MachineMonitorDO update = new MachineMonitorDO();
|
||||
update.setMonitorStatus(MonitorStatus.NOT_START.getStatus());
|
||||
machineMonitorService.updateMonitorConfigByMachineId(machineId, update);
|
||||
// 发送站内信
|
||||
this.sendWebSideMessage(MessageType.MACHINE_AGENT_INSTALL_FAILURE);
|
||||
} finally {
|
||||
Streams.close(session);
|
||||
Streams.close(logStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输文件
|
||||
*
|
||||
* @param pluginDirectory pluginDirectory
|
||||
* @param startScriptPath startScriptPath
|
||||
*/
|
||||
private void transferAgentFile(String pluginDirectory, String startScriptPath) {
|
||||
// 传输脚本目录
|
||||
String agentPath = pluginDirectory + Const.LIB_DIR + "/" + MonitorConst.getAgentFileName();
|
||||
SftpExecutor executor = null;
|
||||
try {
|
||||
// 打开 sftp 连接
|
||||
String charset = machineEnvService.getSftpCharset(machineId);
|
||||
executor = session.getSftpExecutor(charset);
|
||||
executor.connect();
|
||||
// 传输启动脚本文件
|
||||
String startScript = this.getStartScript(agentPath);
|
||||
this.appendLog("开始生成启动脚本 path: {}, command: \n{}", agentPath, startScript);
|
||||
executor.write(startScriptPath, Strings.bytes(startScript));
|
||||
executor.changeMode(startScriptPath, 777);
|
||||
// 传输 agent 文件
|
||||
File localAgentFile = new File(SystemEnvAttr.MACHINE_MONITOR_AGENT_PATH.getValue());
|
||||
// 查询文件是否存在
|
||||
long size = executor.getSize(agentPath);
|
||||
long totalSize = localAgentFile.length();
|
||||
if (totalSize != size) {
|
||||
// 传输文件
|
||||
this.appendLog("插件包不存在-开始传输 {} {}B", agentPath, totalSize);
|
||||
executor.uploadFile(agentPath, localAgentFile);
|
||||
this.appendLog("插件包传输完成 {}", agentPath);
|
||||
} else {
|
||||
this.appendLog("插件包已存在 {}", agentPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.sftp("文件上传失败", e);
|
||||
} finally {
|
||||
Streams.close(executor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 agent 应用
|
||||
*
|
||||
* @param startScriptPath startScriptPath
|
||||
*/
|
||||
private void startAgentApp(String startScriptPath) {
|
||||
CommandExecutor executor = null;
|
||||
try {
|
||||
// 执行启动命令
|
||||
this.appendLog("开始执行启动脚本 path: {}", startScriptPath);
|
||||
// executor = session.getCommandExecutor("bash -l " + startScriptPath);
|
||||
executor = session.getCommandExecutor(startScriptPath);
|
||||
executor.getChannel().setPty(false);
|
||||
CommandExecutors.execCommand(executor, logStream);
|
||||
int exitCode = executor.getExitCode();
|
||||
if (!ExitCode.isSuccess(exitCode)) {
|
||||
throw Exceptions.runtime("执行启动失败");
|
||||
}
|
||||
this.appendLog("命令执行完成 exit: {}", exitCode);
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.runtime("执行启动异常", e);
|
||||
} finally {
|
||||
Streams.close(executor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步检查 agent 状态
|
||||
*/
|
||||
private void checkAgentRunStatus() {
|
||||
// 查询配置
|
||||
MachineMonitorDO monitor = machineMonitorService.selectByMachineId(machineId);
|
||||
// 尝试进行同步 检查是否启动
|
||||
String version = null;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Threads.sleep(Const.MS_S_10);
|
||||
version = machineMonitorService.syncMonitorAgent(machineId, monitor.getMonitorUrl(), monitor.getAccessToken());
|
||||
this.appendLog("检查agent状态 第{}次", i + 1);
|
||||
if (version != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (version == null) {
|
||||
throw Exceptions.runtime("获取 agent 状态失败");
|
||||
}
|
||||
this.appendLog("agent启动成功 version: {}", version);
|
||||
// 更新状态以及版本
|
||||
MachineMonitorDO update = new MachineMonitorDO();
|
||||
update.setMonitorStatus(MonitorStatus.RUNNING.getStatus());
|
||||
update.setAgentVersion(version);
|
||||
machineMonitorService.updateMonitorConfigByMachineId(machineId, update);
|
||||
// 发送站内信
|
||||
this.sendWebSideMessage(MessageType.MACHINE_AGENT_INSTALL_SUCCESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送站内信
|
||||
*/
|
||||
private void sendWebSideMessage(MessageType type) {
|
||||
Map<String, Object> params = Maps.newMap();
|
||||
params.put(EventKeys.NAME, machine.getMachineName());
|
||||
webSideMessageService.addMessage(type, machine.getId(), user.getId(), user.getUsername(), params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启动脚本
|
||||
*
|
||||
* @param agentJarPath agentJar 路径
|
||||
* @return 脚本内容
|
||||
*/
|
||||
private String getStartScript(String agentJarPath) {
|
||||
Map<Object, Object> param = Maps.newMap();
|
||||
param.put("processName", MonitorConst.AGENT_FILE_NAME_PREFIX);
|
||||
param.put("machineId", machineId);
|
||||
param.put("agentJarPath", agentJarPath);
|
||||
return Strings.format(MonitorConst.START_SCRIPT_VALUE, param).replaceAll("\r\n", "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接日志
|
||||
*
|
||||
* @param logString log
|
||||
* @param args args
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void appendLog(String logString, Object... args) {
|
||||
if (!Arrays1.isEmpty(args)) {
|
||||
log.info("安装监控插件-" + logString, args);
|
||||
}
|
||||
if (logStream != null) {
|
||||
logStream.write(Strings.bytes(Strings.format(logString, args)));
|
||||
logStream.write(Letters.LF);
|
||||
logStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.scheduler.SchedulerTaskStatus;
|
||||
import cn.orionsec.ops.dao.SchedulerTaskDAO;
|
||||
import cn.orionsec.ops.dao.SchedulerTaskRecordDAO;
|
||||
import cn.orionsec.ops.entity.domain.SchedulerTaskDO;
|
||||
import cn.orionsec.ops.entity.domain.SchedulerTaskMachineRecordDO;
|
||||
import cn.orionsec.ops.entity.domain.SchedulerTaskRecordDO;
|
||||
import cn.orionsec.ops.handler.scheduler.machine.ITaskMachineHandler;
|
||||
import cn.orionsec.ops.handler.scheduler.machine.TaskMachineHandler;
|
||||
import cn.orionsec.ops.service.api.SchedulerTaskMachineRecordService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractTaskProcessor implements ITaskProcessor {
|
||||
|
||||
protected static final SchedulerTaskDAO schedulerTaskDAO = SpringHolder.getBean(SchedulerTaskDAO.class);
|
||||
|
||||
protected static final SchedulerTaskRecordDAO schedulerTaskRecordDAO = SpringHolder.getBean(SchedulerTaskRecordDAO.class);
|
||||
|
||||
protected static final SchedulerTaskMachineRecordService schedulerTaskMachineRecordService = SpringHolder.getBean(SchedulerTaskMachineRecordService.class);
|
||||
|
||||
protected static final TaskSessionHolder taskSessionHolder = SpringHolder.getBean(TaskSessionHolder.class);
|
||||
|
||||
protected final Long recordId;
|
||||
|
||||
protected SchedulerTaskDO task;
|
||||
|
||||
protected SchedulerTaskRecordDO record;
|
||||
|
||||
protected final Map<Long, ITaskMachineHandler> handlers;
|
||||
|
||||
protected volatile boolean terminated;
|
||||
|
||||
public AbstractTaskProcessor(Long recordId) {
|
||||
this.recordId = recordId;
|
||||
this.handlers = Maps.newLinkedMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("开始执行调度任务-recordId: {}", recordId);
|
||||
// 初始化
|
||||
try {
|
||||
// 填充数据
|
||||
this.getTaskData();
|
||||
// 判断状态
|
||||
if (record == null || !SchedulerTaskStatus.WAIT.getStatus().equals(record.getTaskStatus())) {
|
||||
return;
|
||||
}
|
||||
// 修改状态
|
||||
this.updateStatus(SchedulerTaskStatus.RUNNABLE);
|
||||
} catch (Exception e) {
|
||||
log.error("执行调度任务初始化失败-recordId: {}", recordId, e);
|
||||
this.updateStatus(SchedulerTaskStatus.FAILURE);
|
||||
return;
|
||||
}
|
||||
// 执行
|
||||
Exception ex = null;
|
||||
try {
|
||||
// 添加会话
|
||||
taskSessionHolder.addSession(recordId, this);
|
||||
// 处理
|
||||
this.handler();
|
||||
} catch (Exception e) {
|
||||
// 执行失败
|
||||
ex = e;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.updateStatus(SchedulerTaskStatus.TERMINATED);
|
||||
log.info("执行调度任务执行停止-recordId: {}", recordId);
|
||||
} else if (ex == null) {
|
||||
// 完成回调
|
||||
this.updateStatus(SchedulerTaskStatus.SUCCESS);
|
||||
log.info("执行调度任务执行成功-recordId: {}", recordId);
|
||||
} else {
|
||||
// 异常回调
|
||||
this.updateStatus(SchedulerTaskStatus.FAILURE);
|
||||
log.error("执行调度任务执行失败-recordId: {}", recordId, ex);
|
||||
}
|
||||
} finally {
|
||||
// 释放资源
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理
|
||||
*
|
||||
* @throws Exception 处理异常
|
||||
*/
|
||||
protected abstract void handler() throws Exception;
|
||||
|
||||
/**
|
||||
* 填充数据
|
||||
*/
|
||||
private void getTaskData() {
|
||||
// 查询明细
|
||||
this.record = schedulerTaskRecordDAO.selectById(recordId);
|
||||
if (record == null || !SchedulerTaskStatus.WAIT.getStatus().equals(record.getTaskStatus())) {
|
||||
return;
|
||||
}
|
||||
// 查询任务
|
||||
this.task = schedulerTaskDAO.selectById(record.getTaskId());
|
||||
// 查询机器明细
|
||||
List<SchedulerTaskMachineRecordDO> machineRecords = schedulerTaskMachineRecordService.selectByRecordId(recordId);
|
||||
for (SchedulerTaskMachineRecordDO machineRecord : machineRecords) {
|
||||
handlers.put(machineRecord.getId(), new TaskMachineHandler(machineRecord.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
protected void updateStatus(SchedulerTaskStatus status) {
|
||||
Date now = new Date();
|
||||
// 更新任务
|
||||
SchedulerTaskDO updateTask = new SchedulerTaskDO();
|
||||
updateTask.setId(task.getId());
|
||||
updateTask.setUpdateTime(now);
|
||||
updateTask.setLatelyStatus(status.getStatus());
|
||||
// 更新明细
|
||||
SchedulerTaskRecordDO updateRecord = new SchedulerTaskRecordDO();
|
||||
updateRecord.setId(recordId);
|
||||
updateRecord.setUpdateTime(now);
|
||||
updateRecord.setTaskStatus(status.getStatus());
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
updateRecord.setStartTime(now);
|
||||
break;
|
||||
case SUCCESS:
|
||||
case FAILURE:
|
||||
case TERMINATED:
|
||||
default:
|
||||
updateRecord.setEndTime(now);
|
||||
break;
|
||||
}
|
||||
schedulerTaskDAO.updateById(updateTask);
|
||||
schedulerTaskRecordDAO.updateById(updateRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAll() {
|
||||
this.terminated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateMachine(Long recordMachineId) {
|
||||
ITaskMachineHandler machineHandler = handlers.get(recordMachineId);
|
||||
if (machineHandler != null) {
|
||||
machineHandler.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipMachine(Long recordMachineId) {
|
||||
ITaskMachineHandler machineHandler = handlers.get(recordMachineId);
|
||||
if (machineHandler != null) {
|
||||
machineHandler.skip();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMachine(Long recordMachineId, String command) {
|
||||
ITaskMachineHandler machineHandler = handlers.get(recordMachineId);
|
||||
if (machineHandler != null) {
|
||||
machineHandler.write(command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 移除会话
|
||||
taskSessionHolder.removeSession(recordId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.kit.lang.function.select.Branches;
|
||||
import cn.orionsec.kit.lang.function.select.Selector;
|
||||
import cn.orionsec.ops.constant.common.SerialType;
|
||||
|
||||
public interface ITaskProcessor extends Runnable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 停止全部
|
||||
*/
|
||||
void terminateAll();
|
||||
|
||||
/**
|
||||
* 停止机器操作
|
||||
*
|
||||
* @param recordMachineId recordMachineId
|
||||
*/
|
||||
void terminateMachine(Long recordMachineId);
|
||||
|
||||
/**
|
||||
* 跳过机器操作
|
||||
*
|
||||
* @param recordMachineId recordMachineId
|
||||
*/
|
||||
void skipMachine(Long recordMachineId);
|
||||
|
||||
/**
|
||||
* 发送机器命令
|
||||
*
|
||||
* @param recordMachineId recordMachineId
|
||||
* @param command command
|
||||
*/
|
||||
void writeMachine(Long recordMachineId, String command);
|
||||
|
||||
/**
|
||||
* 获取实际执行处理器
|
||||
*
|
||||
* @param recordId recordId
|
||||
* @param type type
|
||||
* @return 处理器
|
||||
*/
|
||||
static ITaskProcessor with(Long recordId, SerialType type) {
|
||||
return Selector.<SerialType, ITaskProcessor>of(type)
|
||||
.test(Branches.eq(SerialType.SERIAL)
|
||||
.then(() -> new SerialTaskProcessor(recordId)))
|
||||
.test(Branches.eq(SerialType.PARALLEL)
|
||||
.then(() -> new ParallelTaskProcessor(recordId)))
|
||||
.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
|
||||
import cn.orionsec.ops.handler.scheduler.machine.ITaskMachineHandler;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class ParallelTaskProcessor extends AbstractTaskProcessor {
|
||||
|
||||
public ParallelTaskProcessor(Long recordId) {
|
||||
super(recordId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
// 阻塞执行所有任务
|
||||
Collection<ITaskMachineHandler> handlers = this.handlers.values();
|
||||
Threads.blockRun(handlers, SchedulerPools.SCHEDULER_TASK_MACHINE_SCHEDULER);
|
||||
// 检查是否停止
|
||||
if (terminated) {
|
||||
return;
|
||||
}
|
||||
// 全部停止
|
||||
final boolean allTerminated = handlers.stream()
|
||||
.map(ITaskMachineHandler::getStatus)
|
||||
.allMatch(SchedulerTaskMachineStatus.TERMINATED::equals);
|
||||
if (allTerminated) {
|
||||
this.terminated = true;
|
||||
return;
|
||||
}
|
||||
// 全部成功
|
||||
final boolean allSuccess = handlers.stream()
|
||||
.map(ITaskMachineHandler::getStatus)
|
||||
.filter(s -> !SchedulerTaskMachineStatus.TERMINATED.equals(s))
|
||||
.allMatch(SchedulerTaskMachineStatus.SUCCESS::equals);
|
||||
if (!allSuccess) {
|
||||
throw Exceptions.log(MessageConst.OPERATOR_NOT_ALL_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAll() {
|
||||
super.terminateAll();
|
||||
handlers.values().forEach(ITaskMachineHandler::terminate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler;
|
||||
|
||||
import cn.orionsec.ops.constant.common.ExceptionHandlerType;
|
||||
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
|
||||
import cn.orionsec.ops.handler.scheduler.machine.ITaskMachineHandler;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SerialTaskProcessor extends AbstractTaskProcessor {
|
||||
|
||||
public SerialTaskProcessor(Long recordId) {
|
||||
super(recordId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() throws Exception {
|
||||
// 异常处理策略
|
||||
final boolean errorSkipAll = ExceptionHandlerType.SKIP_ALL.getType().equals(task.getExceptionHandler());
|
||||
// 串行执行
|
||||
Exception ex = null;
|
||||
Collection<ITaskMachineHandler> handlers = this.handlers.values();
|
||||
for (ITaskMachineHandler handler : handlers) {
|
||||
// 停止跳过
|
||||
if (terminated) {
|
||||
handler.skip();
|
||||
continue;
|
||||
}
|
||||
// 发生异常并且异常处理策略是跳过所有则跳过
|
||||
if (ex != null && errorSkipAll) {
|
||||
handler.skip();
|
||||
continue;
|
||||
}
|
||||
// 执行
|
||||
try {
|
||||
handler.run();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
}
|
||||
// 异常返回
|
||||
if (ex != null) {
|
||||
throw ex;
|
||||
}
|
||||
// 全部停止
|
||||
final boolean allTerminated = handlers.stream()
|
||||
.map(ITaskMachineHandler::getStatus)
|
||||
.filter(s -> !SchedulerTaskMachineStatus.SKIPPED.equals(s))
|
||||
.allMatch(SchedulerTaskMachineStatus.TERMINATED::equals);
|
||||
if (allTerminated) {
|
||||
this.terminated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminateAll() {
|
||||
super.terminateAll();
|
||||
// 获取当前执行中的机器执行器
|
||||
handlers.values().stream()
|
||||
.filter(s -> s.getStatus().equals(SchedulerTaskMachineStatus.RUNNABLE))
|
||||
.forEach(ITaskMachineHandler::terminate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class TaskSessionHolder {
|
||||
|
||||
/**
|
||||
* key: recordId
|
||||
* value: ITaskProcessor
|
||||
*/
|
||||
private final Map<Long, ITaskProcessor> holder = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加session
|
||||
*
|
||||
* @param id id
|
||||
* @param session session
|
||||
*/
|
||||
public void addSession(Long id, ITaskProcessor session) {
|
||||
holder.put(id, session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public ITaskProcessor getSession(Long id) {
|
||||
return holder.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除session
|
||||
*
|
||||
* @param id id
|
||||
* @return session
|
||||
*/
|
||||
public ITaskProcessor removeSession(Long id) {
|
||||
return holder.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler.machine;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
|
||||
|
||||
public interface ITaskMachineHandler extends Runnable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 跳过 (未开始)
|
||||
*/
|
||||
void skip();
|
||||
|
||||
/**
|
||||
* 停止 (进行中)
|
||||
*/
|
||||
void terminate();
|
||||
|
||||
/**
|
||||
* 发送命令
|
||||
*
|
||||
* @param command command
|
||||
*/
|
||||
void write(String command);
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*
|
||||
* @return 获取状态
|
||||
*/
|
||||
SchedulerTaskMachineStatus getStatus();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
|
||||
package cn.orionsec.ops.handler.scheduler.machine;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.Letters;
|
||||
import cn.orionsec.kit.lang.exception.DisabledException;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.ssh.ExitCode;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutors;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.common.StainCode;
|
||||
import cn.orionsec.ops.constant.scheduler.SchedulerTaskMachineStatus;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.dao.SchedulerTaskMachineRecordDAO;
|
||||
import cn.orionsec.ops.entity.domain.SchedulerTaskMachineRecordDO;
|
||||
import cn.orionsec.ops.handler.tail.TailSessionHolder;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
public class TaskMachineHandler implements ITaskMachineHandler {
|
||||
|
||||
private static final SchedulerTaskMachineRecordDAO schedulerTaskMachineRecordDAO = SpringHolder.getBean(SchedulerTaskMachineRecordDAO.class);
|
||||
|
||||
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private static final TailSessionHolder tailSessionHolder = SpringHolder.getBean(TailSessionHolder.class);
|
||||
|
||||
private final Long machineRecordId;
|
||||
|
||||
private String logPath;
|
||||
|
||||
private OutputStream logOutputStream;
|
||||
|
||||
private SessionStore sessionStore;
|
||||
|
||||
private CommandExecutor executor;
|
||||
|
||||
private SchedulerTaskMachineRecordDO machineRecord;
|
||||
|
||||
private Date startTime, endTime;
|
||||
|
||||
private Integer exitCode;
|
||||
|
||||
private volatile boolean terminated;
|
||||
|
||||
@Getter
|
||||
private volatile SchedulerTaskMachineStatus status;
|
||||
|
||||
public TaskMachineHandler(Long machineRecordId) {
|
||||
this.machineRecordId = machineRecordId;
|
||||
this.status = SchedulerTaskMachineStatus.WAIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// 检查状态
|
||||
log.info("调度任务-机器操作-开始 machineRecordId: {}", machineRecordId);
|
||||
this.machineRecord = schedulerTaskMachineRecordDAO.selectById(machineRecordId);
|
||||
this.status = SchedulerTaskMachineStatus.of(machineRecord.getExecStatus());
|
||||
if (!SchedulerTaskMachineStatus.WAIT.equals(status)) {
|
||||
return;
|
||||
}
|
||||
// 执行
|
||||
Exception ex = null;
|
||||
try {
|
||||
this.updateStatus(SchedulerTaskMachineStatus.RUNNABLE);
|
||||
// 打开日志
|
||||
this.openLogger();
|
||||
// 打开机器
|
||||
this.sessionStore = machineInfoService.openSessionStore(machineRecord.getMachineId());
|
||||
// 获取执行器
|
||||
this.executor = sessionStore.getCommandExecutor(Strings.replaceCRLF(machineRecord.getExecCommand()));
|
||||
// 开始执行
|
||||
CommandExecutors.execCommand(executor, logOutputStream);
|
||||
this.exitCode = executor.getExitCode();
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
}
|
||||
// 回调
|
||||
try {
|
||||
if (terminated) {
|
||||
// 停止回调
|
||||
this.terminatedCallback();
|
||||
} else if (ex == null) {
|
||||
// 完成回调
|
||||
this.completeCallback();
|
||||
} else if (ex instanceof DisabledException) {
|
||||
// 机器未启用回调
|
||||
this.machineDisableCallback();
|
||||
} else {
|
||||
// 执行异常回调
|
||||
this.exceptionCallback(ex);
|
||||
throw Exceptions.runtime(ex);
|
||||
}
|
||||
} finally {
|
||||
// 释放资源
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止回调
|
||||
*/
|
||||
private void terminatedCallback() {
|
||||
log.error("调度任务-机器操作-停止 machineRecordId: {}", machineRecordId);
|
||||
// 更新状态
|
||||
this.updateStatus(SchedulerTaskMachineStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder(Const.LF_2)
|
||||
.append(Utils.getStainKeyWords("# 调度任务执行停止", StainCode.GLOSS_YELLOW))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成回调
|
||||
*/
|
||||
private void completeCallback() {
|
||||
log.info("调度任务-机器操作-完成 machineRecordId: {}, exitCode: {}", machineRecordId, exitCode);
|
||||
final boolean execSuccess = ExitCode.isSuccess(exitCode);
|
||||
// 更新状态
|
||||
if (execSuccess) {
|
||||
this.updateStatus(SchedulerTaskMachineStatus.SUCCESS);
|
||||
} else {
|
||||
this.updateStatus(SchedulerTaskMachineStatus.FAILURE);
|
||||
}
|
||||
// 拼接日志
|
||||
long used = endTime.getTime() - startTime.getTime();
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Letters.LF)
|
||||
.append(Utils.getStainKeyWords("# 调度任务执行完成", StainCode.GLOSS_GREEN))
|
||||
.append(Letters.LF);
|
||||
log.append("exitcode: ")
|
||||
.append(execSuccess
|
||||
? Utils.getStainKeyWords(exitCode, StainCode.GLOSS_BLUE)
|
||||
: Utils.getStainKeyWords(exitCode, StainCode.GLOSS_RED))
|
||||
.append(Letters.LF);
|
||||
log.append("结束时间: ")
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(" used ")
|
||||
.append(Utils.getStainKeyWords(Utils.interval(used), StainCode.GLOSS_BLUE))
|
||||
.append(" (")
|
||||
.append(StainCode.prefix(StainCode.GLOSS_BLUE))
|
||||
.append(used)
|
||||
.append("ms")
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(")\n");
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器未启用回调
|
||||
*/
|
||||
private void machineDisableCallback() {
|
||||
log.error("调度任务-机器操作-机器停用停止 machineRecordId: {}", machineRecordId);
|
||||
// 更新状态
|
||||
this.updateStatus(SchedulerTaskMachineStatus.TERMINATED);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Const.LF)
|
||||
.append(Utils.getStainKeyWords("# 调度任务执行机器未启用", StainCode.GLOSS_YELLOW))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常回调
|
||||
*/
|
||||
private void exceptionCallback(Exception e) {
|
||||
log.error("调度任务-机器操作-失败 machineRecordId: {}", machineRecordId, e);
|
||||
// 更新状态
|
||||
this.updateStatus(SchedulerTaskMachineStatus.FAILURE);
|
||||
// 拼接日志
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Const.LF)
|
||||
.append(Utils.getStainKeyWords("# 调度任务执行失败", StainCode.GLOSS_RED))
|
||||
.append(Letters.TAB)
|
||||
.append(Utils.getStainKeyWords(Dates.format(endTime), StainCode.GLOSS_BLUE))
|
||||
.append(Letters.LF)
|
||||
.append(Exceptions.getStackTraceAsString(e))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip() {
|
||||
log.info("调度任务-机器操作-跳过 machineRecordId: {}, status: {}", machineRecordId, status);
|
||||
if (SchedulerTaskMachineStatus.WAIT.equals(status)) {
|
||||
// 只能跳过等待中的任务
|
||||
this.updateStatus(SchedulerTaskMachineStatus.SKIPPED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
log.info("调度任务-机器操作-停止 machineRecordId: {}", machineRecordId);
|
||||
// 只能停止进行中的任务
|
||||
if (SchedulerTaskMachineStatus.RUNNABLE.equals(status)) {
|
||||
this.terminated = true;
|
||||
Streams.close(this.executor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String command) {
|
||||
executor.write(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开日志
|
||||
*/
|
||||
private void openLogger() {
|
||||
File logFile = new File(Files1.getPath(SystemEnvAttr.LOG_PATH.getValue(), machineRecord.getLogPath()));
|
||||
Files1.touch(logFile);
|
||||
this.logPath = logFile.getAbsolutePath();
|
||||
// 打开日志流
|
||||
log.info("TaskMachineHandler-打开日志流 {} {}", machineRecordId, logPath);
|
||||
this.logOutputStream = Files1.openOutputStreamFastSafe(logFile);
|
||||
// 拼接开始日志
|
||||
StringBuilder log = new StringBuilder()
|
||||
.append(Utils.getStainKeyWords("# 开始执行调度任务 ", StainCode.GLOSS_GREEN))
|
||||
.append(Const.LF);
|
||||
log.append("执行机器: ")
|
||||
.append(Utils.getStainKeyWords(machineRecord.getMachineName(), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF);
|
||||
log.append("开始时间: ")
|
||||
.append(Utils.getStainKeyWords(Dates.format(startTime), StainCode.GLOSS_BLUE))
|
||||
.append(Const.LF_2);
|
||||
log.append(Utils.getStainKeyWords("# 执行命令", StainCode.GLOSS_GREEN))
|
||||
.append(Const.LF)
|
||||
.append(StainCode.prefix(StainCode.GLOSS_CYAN))
|
||||
.append(Utils.getEndLfWithEof(machineRecord.getExecCommand()))
|
||||
.append(StainCode.SUFFIX)
|
||||
.append(Utils.getStainKeyWords("# 开始执行", StainCode.GLOSS_GREEN))
|
||||
.append(Const.LF);
|
||||
this.appendLog(log.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接日志
|
||||
*
|
||||
* @param log log
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void appendLog(String log) {
|
||||
logOutputStream.write(Strings.bytes(log));
|
||||
logOutputStream.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
private void updateStatus(SchedulerTaskMachineStatus status) {
|
||||
Date now = new Date();
|
||||
this.status = status;
|
||||
SchedulerTaskMachineRecordDO update = new SchedulerTaskMachineRecordDO();
|
||||
update.setId(machineRecordId);
|
||||
update.setExecStatus(status.getStatus());
|
||||
update.setUpdateTime(now);
|
||||
switch (status) {
|
||||
case RUNNABLE:
|
||||
this.startTime = now;
|
||||
update.setStartTime(now);
|
||||
break;
|
||||
case SUCCESS:
|
||||
case FAILURE:
|
||||
case TERMINATED:
|
||||
if (startTime != null) {
|
||||
this.endTime = now;
|
||||
update.setEndTime(now);
|
||||
update.setExitCode(exitCode);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
schedulerTaskMachineRecordDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 释放资源
|
||||
Streams.close(executor);
|
||||
Streams.close(sessionStore);
|
||||
Streams.close(logOutputStream);
|
||||
// 异步关闭正在tail的日志
|
||||
tailSessionHolder.asyncCloseTailFile(Const.HOST_MACHINE_ID, logPath);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp;
|
||||
|
||||
import cn.orionsec.kit.lang.support.progress.ByteTransferRateProgress;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.math.Numbers;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.sftp.SftpTransferStatus;
|
||||
import cn.orionsec.ops.dao.FileTransferLogDAO;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyProgressDTO;
|
||||
import cn.orionsec.ops.service.api.MachineEnvService;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public abstract class FileTransferProcessor implements IFileTransferProcessor {
|
||||
|
||||
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
protected static MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
|
||||
|
||||
protected static FileTransferLogDAO fileTransferLogDAO = SpringHolder.getBean(FileTransferLogDAO.class);
|
||||
|
||||
protected static TransferProcessorManager transferProcessorManager = SpringHolder.getBean(TransferProcessorManager.class);
|
||||
|
||||
protected SessionStore sessionStore;
|
||||
|
||||
protected SftpExecutor executor;
|
||||
|
||||
protected FileTransferLogDO record;
|
||||
|
||||
protected Long userId;
|
||||
|
||||
protected Long machineId;
|
||||
|
||||
protected String fileToken;
|
||||
|
||||
protected ByteTransferRateProgress progress;
|
||||
|
||||
protected volatile boolean userCancel;
|
||||
|
||||
public FileTransferProcessor(FileTransferLogDO record) {
|
||||
this.record = record;
|
||||
this.fileToken = record.getFileToken();
|
||||
this.userId = record.getUserId();
|
||||
this.machineId = record.getMachineId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// 判断是否可以传输
|
||||
this.record = fileTransferLogDAO.selectById(record.getId());
|
||||
if (record == null || !SftpTransferStatus.WAIT.getStatus().equals(record.getTransferStatus())) {
|
||||
return;
|
||||
}
|
||||
transferProcessorManager.addProcessor(fileToken, this);
|
||||
try {
|
||||
// 开始传输
|
||||
this.updateStatusAndNotify(SftpTransferStatus.RUNNABLE.getStatus());
|
||||
// 打开连接
|
||||
this.sessionStore = machineInfoService.openSessionStore(machineId);
|
||||
String charset = machineEnvService.getSftpCharset(machineId);
|
||||
this.executor = sessionStore.getSftpExecutor(charset);
|
||||
executor.connect();
|
||||
log.info("sftp传输文件-初始化完毕, 准备处理传输 fileToken: {}", fileToken);
|
||||
// 检查是否可以用文件系统传输
|
||||
if (SftpSupport.checkUseFileSystem(executor)) {
|
||||
// 直接拷贝
|
||||
SftpSupport.usingFsCopy(this);
|
||||
} else {
|
||||
// 处理
|
||||
this.handler();
|
||||
}
|
||||
log.info("sftp传输文件-传输完毕 fileToken: {}", fileToken);
|
||||
} catch (Exception e) {
|
||||
log.error("sftp传输文件-出现异常 fileToken: {}, e: {}, message: {}", fileToken, e.getClass().getName(), e.getMessage());
|
||||
// 程序错误并非传输错误修改状态
|
||||
if (!userCancel) {
|
||||
log.error("sftp传输文件-运行异常 fileToken: {}", fileToken, e);
|
||||
this.updateStatusAndNotify(SftpTransferStatus.ERROR.getStatus());
|
||||
}
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
transferProcessorManager.removeProcessor(fileToken);
|
||||
this.disconnected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
log.info("sftp传输文件-用户暂停 fileToken: {}", fileToken);
|
||||
this.userCancel = true;
|
||||
this.updateStatusAndNotify(SftpTransferStatus.PAUSE.getStatus());
|
||||
this.disconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理操作
|
||||
*/
|
||||
protected abstract void handler();
|
||||
|
||||
/**
|
||||
* 初始化进度条
|
||||
*/
|
||||
protected void initProgress(ByteTransferRateProgress progress) {
|
||||
this.progress = progress;
|
||||
progress.computeRate();
|
||||
progress.rateExecutor(SchedulerPools.SFTP_TRANSFER_RATE_SCHEDULER);
|
||||
progress.rateAcceptor(this::transferAccept);
|
||||
progress.callback(this::transferDoneCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输回调
|
||||
*
|
||||
* @param progress progress
|
||||
*/
|
||||
protected void transferAccept(ByteTransferRateProgress progress) {
|
||||
try {
|
||||
if (progress.isDone()) {
|
||||
return;
|
||||
}
|
||||
String progressRate = Numbers.setScale(progress.getProgress() * 100, 2);
|
||||
String transferRate = Files1.getSize(progress.getNowRate());
|
||||
String transferCurrent = Files1.getSize(progress.getCurrent());
|
||||
// debug
|
||||
// log.info(transferCurrent + " " + progressRate + "% " + transferRate + "/s");
|
||||
// notify progress
|
||||
this.notifyProgress(transferRate, transferCurrent, progressRate);
|
||||
} catch (Exception e) {
|
||||
log.error("sftp-传输信息回调异常 fileToken: {}, digest: {}", fileToken, Exceptions.getDigest(e), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输完成回调
|
||||
*
|
||||
* @param pro progress
|
||||
*/
|
||||
protected void transferDoneCallback() {
|
||||
try {
|
||||
FileTransferLogDO update = new FileTransferLogDO();
|
||||
update.setId(record.getId());
|
||||
if (progress.isError()) {
|
||||
// 非用户取消更新状态
|
||||
if (!userCancel) {
|
||||
this.updateStatusAndNotify(SftpTransferStatus.ERROR.getStatus());
|
||||
}
|
||||
} else {
|
||||
String transferCurrent = Files1.getSize(progress.getCurrent());
|
||||
String transferRate = Files1.getSize(progress.getNowRate());
|
||||
// notify progress
|
||||
this.notifyProgress(transferRate, transferCurrent, "100");
|
||||
// notify status
|
||||
this.updateStatusAndNotify(SftpTransferStatus.FINISH.getStatus(), 100D, progress.getEnd());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("sftp-传输完成回调异常 fileToken: {}, digest: {}", fileToken, Exceptions.getDigest(e));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateStatusAndNotify(Integer status) {
|
||||
if (progress == null) {
|
||||
this.updateStatusAndNotify(status, null, null);
|
||||
} else {
|
||||
this.updateStatusAndNotify(status, progress.getProgress() * 100, progress.getCurrent());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态并且通知
|
||||
*
|
||||
* @param status status
|
||||
* @param progress progress
|
||||
* @param currentSize currentSize
|
||||
*/
|
||||
protected void updateStatusAndNotify(Integer status, Double progress, Long currentSize) {
|
||||
Long id = record.getId();
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
record.setTransferStatus(status);
|
||||
// 更新
|
||||
FileTransferLogDO update = new FileTransferLogDO();
|
||||
update.setId(id);
|
||||
update.setTransferStatus(status);
|
||||
update.setNowProgress(progress);
|
||||
update.setCurrentSize(currentSize);
|
||||
int effect = fileTransferLogDAO.updateById(update);
|
||||
log.info("sftp传输文件-更新状态 fileToken: {}, status: {}, progress: {}, currentSize: {}, effect: {}", fileToken, status, progress, currentSize, effect);
|
||||
// notify status
|
||||
transferProcessorManager.notifySessionStatusEvent(userId, machineId, fileToken, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知进度
|
||||
*
|
||||
* @param rate 速度
|
||||
* @param current 当前位置
|
||||
* @param progress 进度
|
||||
*/
|
||||
protected void notifyProgress(String rate, String current, String progress) {
|
||||
FileTransferNotifyProgressDTO notifyProgress = new FileTransferNotifyProgressDTO(rate, current, progress);
|
||||
transferProcessorManager.notifySessionProgressEvent(userId, machineId, fileToken, notifyProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
protected void disconnected() {
|
||||
if (executor != null) {
|
||||
executor.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp;
|
||||
|
||||
import cn.orionsec.kit.lang.able.Executable;
|
||||
import cn.orionsec.kit.lang.able.Stoppable;
|
||||
import cn.orionsec.kit.lang.function.select.Branches;
|
||||
import cn.orionsec.kit.lang.function.select.Selector;
|
||||
import cn.orionsec.ops.constant.sftp.SftpTransferType;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import cn.orionsec.ops.handler.sftp.hint.FilePackageHint;
|
||||
import cn.orionsec.ops.handler.sftp.hint.FileTransferHint;
|
||||
import cn.orionsec.ops.handler.sftp.impl.DownloadFileProcessor;
|
||||
import cn.orionsec.ops.handler.sftp.impl.PackageFileProcessor;
|
||||
import cn.orionsec.ops.handler.sftp.impl.UploadFileProcessor;
|
||||
|
||||
public interface IFileTransferProcessor extends Runnable, Stoppable, Executable {
|
||||
|
||||
/**
|
||||
* 获取执行processor
|
||||
*
|
||||
* @param hint hint
|
||||
* @return IFileTransferProcessor
|
||||
*/
|
||||
static IFileTransferProcessor of(FileTransferHint hint) {
|
||||
FileTransferLogDO record = hint.getRecord();
|
||||
return Selector.<SftpTransferType, IFileTransferProcessor>of(hint.getType())
|
||||
.test(Branches.eq(SftpTransferType.UPLOAD)
|
||||
.then(() -> new UploadFileProcessor(record)))
|
||||
.test(Branches.eq(SftpTransferType.DOWNLOAD)
|
||||
.then(() -> new DownloadFileProcessor(record)))
|
||||
.test(Branches.eq(SftpTransferType.PACKAGE)
|
||||
.then(() -> new PackageFileProcessor(record, ((FilePackageHint) hint).getFileList())))
|
||||
.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.entity.domain.MachineInfoDO;
|
||||
import cn.orionsec.ops.service.api.MachineEnvService;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.service.api.SftpService;
|
||||
import cn.orionsec.ops.utils.EventParamsHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SftpBasicExecutorHolder {
|
||||
|
||||
@Resource
|
||||
private MachineInfoService machineInfoService;
|
||||
|
||||
@Resource
|
||||
private MachineEnvService machineEnvService;
|
||||
|
||||
@Resource
|
||||
private SftpService sftpService;
|
||||
|
||||
/**
|
||||
* 基本操作的executor 不包含(upload, download)
|
||||
* machineId: executor
|
||||
*/
|
||||
private final Map<Long, SftpExecutor> basicExecutorHolder = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 基本操作executor 最后使用时间
|
||||
*/
|
||||
private final Map<Long, Long> executorUsing = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 获取 sftp 基本操作 executor
|
||||
*
|
||||
* @param sessionToken sessionToken
|
||||
* @return SftpExecutor
|
||||
*/
|
||||
public SftpExecutor getBasicExecutor(String sessionToken) {
|
||||
// 获取executor
|
||||
Long machineId = sftpService.getMachineId(sessionToken);
|
||||
EventParamsHolder.addParam(EventKeys.MACHINE_ID, machineId);
|
||||
return this.getBasicExecutor(machineId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取 sftp 基本操作 executor
|
||||
*
|
||||
* @param machineId machineId
|
||||
* @return SftpExecutor
|
||||
*/
|
||||
public SftpExecutor getBasicExecutor(Long machineId) {
|
||||
return this.getBasicExecutor(machineId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 sftp 基本操作 executor
|
||||
*
|
||||
* @param machineId machineId
|
||||
* @param machine machine
|
||||
* @return SftpExecutor
|
||||
*/
|
||||
public SftpExecutor getBasicExecutor(Long machineId, MachineInfoDO machine) {
|
||||
SftpExecutor executor = basicExecutorHolder.get(machineId);
|
||||
if (executor != null) {
|
||||
if (!executor.isConnected()) {
|
||||
try {
|
||||
executor.connect();
|
||||
} catch (Exception e) {
|
||||
// 无法连接则重新创建实例
|
||||
executor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果没有重新建立连接
|
||||
if (executor == null) {
|
||||
if (machine == null) {
|
||||
machine = machineInfoService.selectById(machineId);
|
||||
}
|
||||
// 获取charset
|
||||
String charset = machineEnvService.getSftpCharset(machineId);
|
||||
// 打开sftp连接
|
||||
SessionStore sessionStore = machineInfoService.openSessionStore(machine);
|
||||
executor = sessionStore.getSftpExecutor(charset);
|
||||
executor.connect();
|
||||
basicExecutorHolder.put(machineId, executor);
|
||||
}
|
||||
executorUsing.put(machineId, System.currentTimeMillis());
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 无效化一段时间(1分钟)未使用的执行器
|
||||
*/
|
||||
@Scheduled(cron = "0 */1 * * * ?")
|
||||
private void invalidationUnusedExecutor() {
|
||||
long curr = System.currentTimeMillis();
|
||||
// 查询需要淘汰的executor的key
|
||||
List<Long> expireKeys = basicExecutorHolder.keySet().stream()
|
||||
.filter(key -> curr > executorUsing.get(key) + Const.MS_S_60 * 5)
|
||||
.collect(Collectors.toList());
|
||||
// 移除
|
||||
expireKeys.forEach(key -> {
|
||||
SftpExecutor sftpExecutor = basicExecutorHolder.get(key);
|
||||
if (sftpExecutor == null) {
|
||||
return;
|
||||
}
|
||||
log.info("淘汰sftp执行器: {}", key);
|
||||
basicExecutorHolder.remove(key);
|
||||
sftpExecutor.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp;
|
||||
|
||||
import cn.orionsec.kit.lang.id.UUIds;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
|
||||
import cn.orionsec.ops.constant.sftp.SftpTransferStatus;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.handler.sftp.impl.UploadFileProcessor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@Slf4j
|
||||
public class SftpSupport {
|
||||
|
||||
private SftpSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查远程机器和本机是否是同一台机器
|
||||
*
|
||||
* @param executor executor
|
||||
* @return 是否为本机
|
||||
*/
|
||||
public static boolean checkUseFileSystem(SftpExecutor executor) {
|
||||
try {
|
||||
// 创建一个临时文件
|
||||
String checkPath = Files1.getPath(SystemEnvAttr.TEMP_PATH.getValue(), UUIds.random32() + ".ck");
|
||||
File checkFile = new File(checkPath);
|
||||
Files1.touch(checkFile);
|
||||
checkFile.deleteOnExit();
|
||||
// 查询远程机器是否有此文件 如果有则证明传输机器和宿主机是同一台
|
||||
boolean exist = executor.getFile(checkFile.getAbsolutePath()) != null;
|
||||
Files1.delete(checkFile);
|
||||
return exist;
|
||||
} catch (Exception e) {
|
||||
log.error("无法使用FSC {}", Exceptions.getDigest(e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 file system copy
|
||||
*
|
||||
* @param processor processor
|
||||
*/
|
||||
public static void usingFsCopy(FileTransferProcessor processor) {
|
||||
String remoteFile = processor.record.getRemoteFile();
|
||||
String localFile = processor.record.getLocalFile();
|
||||
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
|
||||
log.info("sftp文件传输-使用FSC fileToken: {}, machineId: {}, local: {}, remote: {}",
|
||||
processor.fileToken, processor.machineId, localAbsolutePath, remoteFile);
|
||||
// 复制
|
||||
File sourceFile;
|
||||
File targetFile;
|
||||
if (processor instanceof UploadFileProcessor) {
|
||||
sourceFile = new File(localAbsolutePath);
|
||||
targetFile = new File(remoteFile);
|
||||
} else {
|
||||
sourceFile = new File(remoteFile);
|
||||
targetFile = new File(localAbsolutePath);
|
||||
}
|
||||
Files1.copy(sourceFile, targetFile);
|
||||
// 通知进度
|
||||
long fileSize = sourceFile.length();
|
||||
processor.notifyProgress(Files1.getSize(fileSize), Files1.getSize(fileSize), "100");
|
||||
// 通知状态
|
||||
processor.updateStatusAndNotify(SftpTransferStatus.FINISH.getStatus(), 100D, fileSize);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.convert.Converts;
|
||||
import cn.orionsec.kit.lang.utils.json.Jsons;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.sftp.SftpNotifyType;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyDTO;
|
||||
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyProgressDTO;
|
||||
import cn.orionsec.ops.entity.vo.sftp.FileTransferLogVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TransferProcessorManager {
|
||||
|
||||
/**
|
||||
* key: token
|
||||
* value: processor
|
||||
*/
|
||||
private final Map<String, IFileTransferProcessor> transferProcessor = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* sessionId 和 session 映射
|
||||
* <p>
|
||||
* key: sessionId
|
||||
* value: webSocketSession
|
||||
*/
|
||||
private final Map<String, WebSocketSession> idMapping = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 机器 和 session 映射
|
||||
* <p>
|
||||
* key: userId_machineId
|
||||
* value: sessionIdList
|
||||
*/
|
||||
private final Map<String, List<String>> userMachineSessionMapping = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 文件传输进度
|
||||
* <p>
|
||||
* key: token
|
||||
* value: progress
|
||||
*/
|
||||
private final Map<String, String> TRANSFER_PROGRESS = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加processor
|
||||
*
|
||||
* @param token token
|
||||
* @param processor processor
|
||||
*/
|
||||
public void addProcessor(String token, IFileTransferProcessor processor) {
|
||||
transferProcessor.put(token, processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除processor
|
||||
*
|
||||
* @param token token
|
||||
*/
|
||||
public void removeProcessor(String token) {
|
||||
transferProcessor.remove(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取processor
|
||||
*
|
||||
* @param token token
|
||||
* @return processor
|
||||
*/
|
||||
public IFileTransferProcessor getProcessor(String token) {
|
||||
return transferProcessor.get(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册session 通知
|
||||
*
|
||||
* @param id id
|
||||
* @param session session
|
||||
* @param userId userId
|
||||
* @param machineId machineId
|
||||
*/
|
||||
public void registerSessionNotify(String id, WebSocketSession session, Long userId, Long machineId) {
|
||||
idMapping.put(id, session);
|
||||
userMachineSessionMapping.computeIfAbsent(this.getUserMachine(userId, machineId), s -> Lists.newList()).add(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭session 通知
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
public void closeSessionNotify(String id) {
|
||||
idMapping.remove(id);
|
||||
// 删除机器与会话的关联
|
||||
userMachineSessionMapping.forEach((k, v) -> {
|
||||
if (Lists.isEmpty(v)) {
|
||||
return;
|
||||
}
|
||||
v.removeIf(s -> s.equals(id));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知session 添加事件
|
||||
*
|
||||
* @param userId userId
|
||||
* @param machineId machineId
|
||||
* @param record record
|
||||
*/
|
||||
public void notifySessionAddEvent(Long userId, Long machineId, FileTransferLogDO record) {
|
||||
FileTransferNotifyDTO notify = new FileTransferNotifyDTO();
|
||||
notify.setType(SftpNotifyType.ADD.getType());
|
||||
notify.setFileToken(record.getFileToken());
|
||||
notify.setBody(Jsons.toJsonWriteNull(Converts.to(record, FileTransferLogVO.class)));
|
||||
this.notifySession(userId, machineId, notify);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知session 进度事件
|
||||
*
|
||||
* @param userId userId
|
||||
* @param machineId machineId
|
||||
* @param fileToken fileToken
|
||||
* @param progress progress
|
||||
*/
|
||||
public void notifySessionProgressEvent(Long userId, Long machineId, String fileToken, FileTransferNotifyProgressDTO progress) {
|
||||
// 设置进度
|
||||
TRANSFER_PROGRESS.put(fileToken, progress.getProgress());
|
||||
// 通知
|
||||
FileTransferNotifyDTO notify = new FileTransferNotifyDTO();
|
||||
notify.setType(SftpNotifyType.PROGRESS.getType());
|
||||
notify.setFileToken(fileToken);
|
||||
notify.setBody(Jsons.toJsonWriteNull(progress));
|
||||
this.notifySession(userId, machineId, notify);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知session 状态事件
|
||||
*
|
||||
* @param userId userId
|
||||
* @param machineId machineId
|
||||
* @param fileToken fileToken
|
||||
* @param status status
|
||||
*/
|
||||
public void notifySessionStatusEvent(Long userId, Long machineId, String fileToken, Integer status) {
|
||||
// 清除进度
|
||||
TRANSFER_PROGRESS.remove(fileToken);
|
||||
// 通知
|
||||
FileTransferNotifyDTO notify = new FileTransferNotifyDTO();
|
||||
notify.setType(SftpNotifyType.CHANGE_STATUS.getType());
|
||||
notify.setFileToken(fileToken);
|
||||
notify.setBody(status);
|
||||
this.notifySession(userId, machineId, notify);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知session
|
||||
*
|
||||
* @param userId userId
|
||||
* @param machineId machineId
|
||||
* @param notify notifyInfo
|
||||
*/
|
||||
public void notifySession(Long userId, Long machineId, FileTransferNotifyDTO notify) {
|
||||
List<String> sessionIds = userMachineSessionMapping.get(this.getUserMachine(userId, machineId));
|
||||
if (Lists.isEmpty(sessionIds)) {
|
||||
return;
|
||||
}
|
||||
for (String sessionId : sessionIds) {
|
||||
if (sessionId == null) {
|
||||
continue;
|
||||
}
|
||||
WebSocketSession session = idMapping.get(sessionId);
|
||||
if (session == null || !session.isOpen()) {
|
||||
continue;
|
||||
}
|
||||
// 通知
|
||||
Exception ex = null;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
try {
|
||||
session.sendMessage(new TextMessage(Jsons.toJsonWriteNull(notify)));
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
log.error("通知session失败 userId: {}, machineId: {} sessionId: {}, try: {}, e: {}", userId, machineId, session, (i + 1), Exceptions.getDigest(e));
|
||||
Threads.sleep(Const.N_100);
|
||||
}
|
||||
}
|
||||
if (ex != null) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户机器key
|
||||
*
|
||||
* @param userId userId
|
||||
* @param machineId machineId
|
||||
* @return key
|
||||
*/
|
||||
private String getUserMachine(Long userId, Long machineId) {
|
||||
return userId + "_" + machineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取传输进度
|
||||
*
|
||||
* @param token token
|
||||
* @return progress
|
||||
*/
|
||||
public Double getProgress(String token) {
|
||||
String progress = TRANSFER_PROGRESS.get(token);
|
||||
if (progress == null) {
|
||||
return null;
|
||||
}
|
||||
return Double.valueOf(progress);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.direct;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.kit.lang.utils.Valid;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.sftp.SftpExecutor;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.handler.sftp.SftpSupport;
|
||||
import cn.orionsec.ops.service.api.MachineEnvService;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Slf4j
|
||||
public class DirectDownloader implements SafeCloseable {
|
||||
|
||||
private static final MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
private static final MachineEnvService machineEnvService = SpringHolder.getBean(MachineEnvService.class);
|
||||
|
||||
/**
|
||||
* 机器id
|
||||
*/
|
||||
private final Long machineId;
|
||||
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
private SessionStore session;
|
||||
|
||||
/**
|
||||
* 执行器
|
||||
*/
|
||||
private SftpExecutor executor;
|
||||
|
||||
public DirectDownloader(Long machineId) {
|
||||
this.machineId = machineId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开连接
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
public DirectDownloader open() {
|
||||
log.info("直接下载远程文件-建立连接-开始 machineId: {}", machineId);
|
||||
try {
|
||||
this.session = machineInfoService.openSessionStore(machineId);
|
||||
log.info("直接下载远程文件-建立连接-成功 machineId: {}", machineId);
|
||||
return this;
|
||||
} catch (Exception e) {
|
||||
log.error("直接下载远程文件-建立连接-失败 machineId: {}, e: {}", machineId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件
|
||||
*
|
||||
* @param path path
|
||||
* @return 文件流
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public InputStream getFile(String path) throws IOException {
|
||||
log.info("直接下载远程文件-开始执行 machineId: {}, path: {}", machineId, path);
|
||||
Valid.notNull(session, MessageConst.UNCONNECTED);
|
||||
try {
|
||||
// 打开执行器
|
||||
String charset = machineEnvService.getSftpCharset(machineId);
|
||||
this.executor = session.getSftpExecutor(charset);
|
||||
executor.connect();
|
||||
// 检查是否为本机
|
||||
if (SftpSupport.checkUseFileSystem(executor)) {
|
||||
// 是本机则返回文件流
|
||||
return Files1.openInputStreamFast(path);
|
||||
} else {
|
||||
// 不是本机获取sftp文件
|
||||
return executor.openInputStream(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("直接下载远程文件-执行失败 machineId: {}, path: {}, e: {}", machineId, path, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭执行器
|
||||
*/
|
||||
public void closeExecutor() {
|
||||
Streams.close(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Streams.close(executor);
|
||||
Streams.close(session);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.hint;
|
||||
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class FilePackageHint extends FileTransferHint {
|
||||
|
||||
/**
|
||||
* 压缩文件列表
|
||||
*/
|
||||
private List<FileTransferLogDO> fileList;
|
||||
|
||||
public FilePackageHint(FileTransferLogDO record, List<FileTransferLogDO> fileList) {
|
||||
super(record);
|
||||
this.fileList = fileList;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.hint;
|
||||
|
||||
import cn.orionsec.ops.constant.sftp.SftpTransferType;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class FileTransferHint {
|
||||
|
||||
/**
|
||||
* record
|
||||
*/
|
||||
private FileTransferLogDO record;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private SftpTransferType type;
|
||||
|
||||
public FileTransferHint(FileTransferLogDO record) {
|
||||
this.record = record;
|
||||
this.type = SftpTransferType.of(record.getTransferType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传配置
|
||||
*
|
||||
* @param record record
|
||||
* @return 上传配置
|
||||
*/
|
||||
public static FileTransferHint transfer(FileTransferLogDO record) {
|
||||
return new FileTransferHint(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取打包配置
|
||||
*
|
||||
* @param record record
|
||||
* @param fileList fileList
|
||||
* @return 上传配置
|
||||
*/
|
||||
public static FilePackageHint packaged(FileTransferLogDO record, List<FileTransferLogDO> fileList) {
|
||||
return new FilePackageHint(record, fileList);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.impl;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.net.host.sftp.transfer.SftpDownloader;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import cn.orionsec.ops.handler.sftp.FileTransferProcessor;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class DownloadFileProcessor extends FileTransferProcessor {
|
||||
|
||||
public DownloadFileProcessor(FileTransferLogDO record) {
|
||||
super(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
String localFile = record.getLocalFile();
|
||||
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
|
||||
log.info("sftp文件下载-提交任务 fileToken: {}, machineId: {}, local: {}, remote: {}, record: {}",
|
||||
fileToken, machineId, localAbsolutePath, record.getRemoteFile(), JSON.toJSONString(record));
|
||||
Threads.start(this, SchedulerPools.SFTP_DOWNLOAD_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() {
|
||||
String remoteFile = record.getRemoteFile();
|
||||
String localFile = record.getLocalFile();
|
||||
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
|
||||
log.info("sftp文件下载-开始传输 fileToken: {}, machineId: {}, local: {}, remote: {}",
|
||||
fileToken, machineId, localAbsolutePath, remoteFile);
|
||||
SftpDownloader download = executor.download(remoteFile, localAbsolutePath);
|
||||
this.initProgress(download.getProgress());
|
||||
download.run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.impl;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.io.compress.CompressTypeEnum;
|
||||
import cn.orionsec.kit.lang.utils.io.compress.FileCompressor;
|
||||
import cn.orionsec.kit.lang.utils.math.Numbers;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.sftp.SftpTransferStatus;
|
||||
import cn.orionsec.ops.constant.sftp.SftpTransferType;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.dao.FileTransferLogDAO;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import cn.orionsec.ops.entity.dto.sftp.FileTransferNotifyProgressDTO;
|
||||
import cn.orionsec.ops.handler.sftp.IFileTransferProcessor;
|
||||
import cn.orionsec.ops.handler.sftp.TransferProcessorManager;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
public class PackageFileProcessor implements IFileTransferProcessor {
|
||||
|
||||
protected static FileTransferLogDAO fileTransferLogDAO = SpringHolder.getBean(FileTransferLogDAO.class);
|
||||
|
||||
protected static TransferProcessorManager transferProcessorManager = SpringHolder.getBean(TransferProcessorManager.class);
|
||||
|
||||
private static final String FINISH_PROGRESS = "100";
|
||||
|
||||
/**
|
||||
* 打包文件
|
||||
*/
|
||||
private final FileTransferLogDO packageFile;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private final List<FileTransferLogDO> fileList;
|
||||
|
||||
/**
|
||||
* 文件名映射
|
||||
*/
|
||||
private Map<String, FileTransferLogDO> nameMapping;
|
||||
|
||||
/**
|
||||
* 当前大小
|
||||
*/
|
||||
private final AtomicLong currentSize;
|
||||
|
||||
/**
|
||||
* 文件总大小
|
||||
*/
|
||||
private final long totalSize;
|
||||
|
||||
/**
|
||||
* 文件压缩器
|
||||
*/
|
||||
private FileCompressor compressor;
|
||||
|
||||
/**
|
||||
* 压缩文件路径
|
||||
*/
|
||||
private String compressPath;
|
||||
|
||||
private final Long userId;
|
||||
|
||||
private final Long machineId;
|
||||
|
||||
private final String fileToken;
|
||||
|
||||
private volatile boolean userCancel;
|
||||
|
||||
private volatile boolean done;
|
||||
|
||||
public PackageFileProcessor(FileTransferLogDO packageFile, List<FileTransferLogDO> fileList) {
|
||||
this.packageFile = packageFile;
|
||||
this.fileList = fileList;
|
||||
this.fileToken = packageFile.getFileToken();
|
||||
this.currentSize = new AtomicLong();
|
||||
this.totalSize = packageFile.getFileSize();
|
||||
this.userId = packageFile.getUserId();
|
||||
this.machineId = packageFile.getMachineId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
String localFile = packageFile.getLocalFile();
|
||||
this.compressPath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
|
||||
log.info("sftp文件打包-提交任务 fileToken: {} machineId: {}, local: {}, remote: {}, record: {}, fileList: {}",
|
||||
fileToken, machineId, compressPath, packageFile.getRemoteFile(),
|
||||
JSON.toJSONString(packageFile), JSON.toJSONString(fileList));
|
||||
Threads.start(this, SchedulerPools.SFTP_PACKAGE_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// 判断是否可以传输
|
||||
FileTransferLogDO fileTransferLog = fileTransferLogDAO.selectById(packageFile.getId());
|
||||
if (fileTransferLog == null || !SftpTransferStatus.WAIT.getStatus().equals(fileTransferLog.getTransferStatus())) {
|
||||
return;
|
||||
}
|
||||
transferProcessorManager.addProcessor(fileToken, this);
|
||||
try {
|
||||
// 通知状态runnable
|
||||
this.updateStatus(SftpTransferStatus.RUNNABLE);
|
||||
// 初始化压缩器
|
||||
this.compressor = CompressTypeEnum.ZIP.compressor().get();
|
||||
compressor.setAbsoluteCompressPath(compressPath);
|
||||
compressor.compressNotify(this::notifyProgress);
|
||||
// 添加压缩文件
|
||||
this.initCompressFiles();
|
||||
// 添加压缩清单
|
||||
this.initCompressFileRaw();
|
||||
// 二次检查状态 防止在添加文件过程中取消或者删除
|
||||
fileTransferLog = fileTransferLogDAO.selectById(packageFile.getId());
|
||||
if (fileTransferLog == null || !SftpTransferStatus.RUNNABLE.getStatus().equals(fileTransferLog.getTransferStatus())) {
|
||||
return;
|
||||
}
|
||||
// 开始压缩
|
||||
this.compressor.compress();
|
||||
// 传输完成通知
|
||||
this.updateStatus(SftpTransferStatus.FINISH);
|
||||
} catch (Exception e) {
|
||||
log.error("sftp压缩文件-出现异常 fileToken: {}, e: {}, message: {}", fileToken, e.getClass().getName(), e.getMessage());
|
||||
// 程序错误并非传输错误修改状态
|
||||
if (!userCancel) {
|
||||
log.error("sftp传输文件-运行异常 fileToken: {}", fileToken, e);
|
||||
this.updateStatus(SftpTransferStatus.ERROR);
|
||||
}
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
this.done = true;
|
||||
transferProcessorManager.removeProcessor(fileToken);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
log.info("sftp传输打包-用户取消 fileToken: {}", fileToken);
|
||||
this.userCancel = true;
|
||||
// 修改状态为已取消
|
||||
this.updateStatus(SftpTransferStatus.CANCEL);
|
||||
// 取消
|
||||
if (compressor != null) {
|
||||
Streams.close(compressor.getCloseable());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化压缩文件
|
||||
*/
|
||||
private void initCompressFiles() {
|
||||
this.nameMapping = Maps.newLinkedMap();
|
||||
for (int i = 0; i < fileList.size(); i++) {
|
||||
FileTransferLogDO fileLog = fileList.get(i);
|
||||
String remoteFile = fileLog.getRemoteFile();
|
||||
String localFilePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), fileLog.getLocalFile());
|
||||
if (!Files1.isFile(new File(localFilePath))) {
|
||||
continue;
|
||||
}
|
||||
// 添加mapping
|
||||
String remoteFileName;
|
||||
if (nameMapping.containsKey(remoteFile)) {
|
||||
remoteFileName = remoteFile + "_" + (i + 1);
|
||||
} else {
|
||||
remoteFileName = remoteFile;
|
||||
}
|
||||
nameMapping.put(remoteFileName, fileLog);
|
||||
compressor.addFile(remoteFileName, localFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加压缩文件清单到压缩列表
|
||||
*/
|
||||
private void initCompressFileRaw() {
|
||||
// 设置文件清单
|
||||
List<String> compressFileRaw = Lists.newList();
|
||||
for (FileTransferLogDO fileLog : fileList) {
|
||||
String remoteFile = fileLog.getRemoteFile();
|
||||
String label = SftpTransferType.of(fileLog.getTransferType()).getLabel();
|
||||
String localFilePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), fileLog.getLocalFile());
|
||||
String status = Files1.isFile(localFilePath) ? "成功" : "未找到文件";
|
||||
// 添加raw
|
||||
compressFileRaw.add(label + Const.SPACE + status + Const.SPACE + remoteFile);
|
||||
}
|
||||
// 设置文件清单文件
|
||||
String compressRawListFile = String.join(Const.LF, compressFileRaw) + Const.LF;
|
||||
InputStream compressRawListStream = Streams.toInputStream(compressRawListFile);
|
||||
compressor.addFile(Const.COMPRESS_LIST_FILE, compressRawListStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知状态
|
||||
*
|
||||
* @param status status
|
||||
*/
|
||||
private void updateStatus(SftpTransferStatus status) {
|
||||
FileTransferLogDO update = new FileTransferLogDO();
|
||||
update.setId(packageFile.getId());
|
||||
update.setTransferStatus(status.getStatus());
|
||||
if (SftpTransferStatus.FINISH.equals(status)) {
|
||||
// 设置压缩文件实际大小
|
||||
File compressFile = new File(compressPath);
|
||||
if (Files1.isFile(compressFile)) {
|
||||
update.setFileSize(compressFile.length());
|
||||
update.setCurrentSize(compressFile.length());
|
||||
} else {
|
||||
update.setCurrentSize(packageFile.getFileSize());
|
||||
}
|
||||
update.setNowProgress(100D);
|
||||
}
|
||||
int effect = fileTransferLogDAO.updateById(update);
|
||||
log.info("sftp传输压缩-更新状态 fileToken: {}, status: {}, effect: {}", fileToken, status, effect);
|
||||
if (SftpTransferStatus.FINISH.equals(status)) {
|
||||
// 通知进度
|
||||
FileTransferNotifyProgressDTO notifyProgress = new FileTransferNotifyProgressDTO(Strings.EMPTY, Files1.getSize(totalSize), FINISH_PROGRESS);
|
||||
transferProcessorManager.notifySessionProgressEvent(userId, machineId, fileToken, notifyProgress);
|
||||
}
|
||||
// 通知状态
|
||||
transferProcessorManager.notifySessionStatusEvent(userId, machineId, fileToken, status.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知进度
|
||||
*
|
||||
* @param name name
|
||||
*/
|
||||
private void notifyProgress(String name) {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
FileTransferLogDO compressedFile = nameMapping.get(name);
|
||||
if (compressedFile == null) {
|
||||
return;
|
||||
}
|
||||
// 计算进度
|
||||
long curr = currentSize.addAndGet(compressedFile.getFileSize());
|
||||
double progress = this.getProgress();
|
||||
String progressRate = Numbers.setScale(progress, 2);
|
||||
// 更新进度
|
||||
FileTransferLogDO update = new FileTransferLogDO();
|
||||
update.setId(packageFile.getId());
|
||||
update.setCurrentSize(curr);
|
||||
update.setNowProgress(progress);
|
||||
fileTransferLogDAO.updateById(update);
|
||||
// 通知进度
|
||||
FileTransferNotifyProgressDTO notifyProgress = new FileTransferNotifyProgressDTO(Strings.EMPTY, Files1.getSize(curr), progressRate);
|
||||
transferProcessorManager.notifySessionProgressEvent(userId, machineId, fileToken, notifyProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前进度
|
||||
*
|
||||
* @return 当前进度
|
||||
*/
|
||||
protected double getProgress() {
|
||||
if (totalSize == 0) {
|
||||
return 0;
|
||||
}
|
||||
return ((double) currentSize.get() / (double) totalSize) * 100;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.impl;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.net.host.sftp.transfer.SftpUploader;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.entity.domain.FileTransferLogDO;
|
||||
import cn.orionsec.ops.handler.sftp.FileTransferProcessor;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class UploadFileProcessor extends FileTransferProcessor {
|
||||
|
||||
public UploadFileProcessor(FileTransferLogDO record) {
|
||||
super(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exec() {
|
||||
String localFile = record.getLocalFile();
|
||||
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
|
||||
log.info("sftp文件上传-提交任务 fileToken: {}, machineId: {}, local: {}, remote: {}, record: {}",
|
||||
fileToken, machineId, localAbsolutePath, record.getRemoteFile(), JSON.toJSONString(record));
|
||||
Threads.start(this, SchedulerPools.SFTP_UPLOAD_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handler() {
|
||||
String remoteFile = record.getRemoteFile();
|
||||
String localFile = record.getLocalFile();
|
||||
String localAbsolutePath = Files1.getPath(SystemEnvAttr.SWAP_PATH.getValue(), localFile);
|
||||
log.info("sftp文件上传-开始传输 fileToken: {}, machineId: {}, local: {}, remote: {}",
|
||||
fileToken, machineId, localAbsolutePath, remoteFile);
|
||||
SftpUploader upload = executor.upload(remoteFile, localAbsolutePath);
|
||||
this.initProgress(upload.getProgress());
|
||||
upload.run();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
|
||||
package cn.orionsec.ops.handler.sftp.notify;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.entity.dto.sftp.SftpSessionTokenDTO;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.handler.sftp.TransferProcessorManager;
|
||||
import cn.orionsec.ops.service.api.PassportService;
|
||||
import cn.orionsec.ops.service.api.SftpService;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class FileTransferNotifyHandler implements WebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private TransferProcessorManager transferProcessorManager;
|
||||
|
||||
@Resource
|
||||
private PassportService passportService;
|
||||
|
||||
@Resource
|
||||
private SftpService sftpService;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
String id = session.getId();
|
||||
String token = WebSockets.getToken(session);
|
||||
try {
|
||||
SftpSessionTokenDTO tokenInfo = sftpService.getTokenInfo(token);
|
||||
Long userId = tokenInfo.getUserId();
|
||||
Long machineId = tokenInfo.getMachineId();
|
||||
List<Long> machineIdList = tokenInfo.getMachineIdList();
|
||||
if (machineIdList == null) {
|
||||
machineIdList = Lists.newList();
|
||||
}
|
||||
if (machineId != null) {
|
||||
machineIdList.add(machineId);
|
||||
}
|
||||
session.getAttributes().put(WebSockets.UID, userId);
|
||||
session.getAttributes().put(WebSockets.MID, machineIdList);
|
||||
log.info("sftp-Notify 建立连接成功 id: {}, token: {}, userId: {}, machineId: {}, machineIdList: {}", id, token, userId, machineId, machineIdList);
|
||||
} catch (Exception e) {
|
||||
log.error("sftp-Notify 建立连接失败-未查询到token信息 id: {}, token: {}", id, token, e);
|
||||
WebSockets.close(session, WsCloseCode.FORGE_TOKEN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
String id = session.getId();
|
||||
Map<String, Object> attributes = session.getAttributes();
|
||||
if (attributes.get(WebSockets.AUTHED) != null) {
|
||||
return;
|
||||
}
|
||||
if (!(message instanceof TextMessage)) {
|
||||
return;
|
||||
}
|
||||
// 获取body
|
||||
String authToken = ((TextMessage) message).getPayload();
|
||||
if (Strings.isEmpty(authToken)) {
|
||||
log.info("sftp-Notify 认证失败-body为空 id: {}", id);
|
||||
WebSockets.close(session, WsCloseCode.INCORRECT_TOKEN);
|
||||
return;
|
||||
}
|
||||
// 获取认证用户
|
||||
UserDTO user = passportService.getUserByToken(authToken, null);
|
||||
if (user == null) {
|
||||
log.info("sftp-Notify 认证失败-未查询到用户 id: {}, authToken: {}", id, authToken);
|
||||
WebSockets.close(session, WsCloseCode.INCORRECT_TOKEN);
|
||||
return;
|
||||
}
|
||||
// 检查认证用户是否匹配
|
||||
Long userId = user.getId();
|
||||
Long tokenUserId = (Long) attributes.get(WebSockets.UID);
|
||||
final boolean valid = userId.equals(tokenUserId);
|
||||
if (!valid) {
|
||||
log.info("sftp-Notify 认证失败-用户不匹配 id: {}, userId: {}, tokenUserId: {}", id, userId, tokenUserId);
|
||||
WebSockets.close(session, WsCloseCode.VALID);
|
||||
return;
|
||||
}
|
||||
attributes.put(WebSockets.AUTHED, Const.ENABLE);
|
||||
// 注册会话
|
||||
List<Long> machineIdList = (List<Long>) attributes.get(WebSockets.MID);
|
||||
Lists.forEach(machineIdList, i -> {
|
||||
log.info("sftp-Notify 认证成功 id: {}, userId: {}, machineId: {}", id, userId, i);
|
||||
transferProcessorManager.registerSessionNotify(id, session, userId, i);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||
log.error("sftp-Notify 操作异常拦截 id: {}", session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
String id = session.getId();
|
||||
transferProcessorManager.closeSessionNotify(id);
|
||||
log.info("sftp-Notify 关闭连接 id: {}, code: {}, reason: {}", id, status.getCode(), status.getReason());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPartialMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
package cn.orionsec.ops.handler.tail;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.kit.lang.function.select.Branches;
|
||||
import cn.orionsec.kit.lang.function.select.Selector;
|
||||
import cn.orionsec.ops.constant.tail.FileTailMode;
|
||||
import cn.orionsec.ops.handler.tail.impl.ExecTailFileHandler;
|
||||
import cn.orionsec.ops.handler.tail.impl.TrackerTailFileHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
public interface ITailHandler extends SafeCloseable {
|
||||
|
||||
/**
|
||||
* 开始
|
||||
*
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
void start() throws Exception;
|
||||
|
||||
/**
|
||||
* 获取机器id
|
||||
*
|
||||
* @return 机器id
|
||||
*/
|
||||
Long getMachineId();
|
||||
|
||||
/**
|
||||
* 获取文件路径
|
||||
*
|
||||
* @return 文件路径
|
||||
*/
|
||||
String getFilePath();
|
||||
|
||||
/**
|
||||
* 设置最后修改时间
|
||||
*/
|
||||
default void setLastModify() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入命令
|
||||
*
|
||||
* @param command command
|
||||
*/
|
||||
default void write(String command) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实际执行 handler
|
||||
*
|
||||
* @param mode mode
|
||||
* @param hint hint
|
||||
* @param session session
|
||||
* @return handler
|
||||
*/
|
||||
static ITailHandler with(FileTailMode mode, TailFileHint hint, WebSocketSession session) {
|
||||
return Selector.<FileTailMode, ITailHandler>of(mode)
|
||||
.test(Branches.eq(FileTailMode.TRACKER)
|
||||
.then(() -> new TrackerTailFileHandler(hint, session)))
|
||||
.test(Branches.eq(FileTailMode.TAIL)
|
||||
.then(() -> new ExecTailFileHandler(hint, session)))
|
||||
.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
package cn.orionsec.ops.handler.tail;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.constant.tail.FileTailMode;
|
||||
import cn.orionsec.ops.entity.dto.file.FileTailDTO;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TailFileHandler implements WebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private TailSessionHolder tailSessionHolder;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
String id = session.getId();
|
||||
FileTailDTO config = (FileTailDTO) session.getAttributes().get(WebSockets.CONFIG);
|
||||
String token = (String) session.getAttributes().get(WebSockets.TOKEN);
|
||||
log.info("tail 建立ws连接 token: {}, id: {}, config: {}", token, id, JSON.toJSONString(config));
|
||||
try {
|
||||
this.openTailHandler(session, config, token);
|
||||
} catch (Exception e) {
|
||||
log.error("tail 打开处理器-失败 id: {}", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||
log.error("tail 操作异常拦截 token: {}, id: {}", WebSockets.getToken(session), session.getId(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
String token = (String) session.getAttributes().get(WebSockets.TOKEN);
|
||||
// 释放资源
|
||||
ITailHandler handler = tailSessionHolder.removeSession(token);
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
handler.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPartialMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文件处理器
|
||||
*
|
||||
* @param session session
|
||||
* @param fileTail tailDTO
|
||||
* @param token token
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
private void openTailHandler(WebSocketSession session, FileTailDTO fileTail, String token) throws Exception {
|
||||
TailFileHint hint = new TailFileHint();
|
||||
hint.setToken(token);
|
||||
hint.setMachineId(fileTail.getMachineId());
|
||||
hint.setPath(fileTail.getFilePath());
|
||||
hint.setOffset(fileTail.getOffset());
|
||||
hint.setCharset(fileTail.getCharset());
|
||||
hint.setCommand(fileTail.getCommand());
|
||||
FileTailMode mode = FileTailMode.of(fileTail.getMode());
|
||||
if (FileTailMode.TRACKER.equals(mode)) {
|
||||
// 获取 delay
|
||||
String delayValue = SystemEnvAttr.TRACKER_DELAY_TIME.getValue();
|
||||
int delay = Strings.isInteger(delayValue) ? Integer.parseInt(delayValue) : Const.TRACKER_DELAY_MS;
|
||||
hint.setDelay(Math.max(delay, Const.MIN_TRACKER_DELAY_MS));
|
||||
}
|
||||
log.info("tail 打开处理器-开始 token: {}, mode: {}, hint: {}", token, mode, JSON.toJSONString(hint));
|
||||
ITailHandler handler = ITailHandler.with(mode, hint, session);
|
||||
tailSessionHolder.addSession(token, handler);
|
||||
handler.start();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
package cn.orionsec.ops.handler.tail;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TailFileHint {
|
||||
|
||||
/**
|
||||
* token
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 文件
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 机器id
|
||||
*/
|
||||
private Long machineId;
|
||||
|
||||
/**
|
||||
* 尾行偏移量
|
||||
*/
|
||||
private Integer offset;
|
||||
|
||||
/**
|
||||
* 编码格式
|
||||
*/
|
||||
private String charset;
|
||||
|
||||
/**
|
||||
* tail 命令
|
||||
*/
|
||||
private String command;
|
||||
|
||||
/**
|
||||
* 延迟时间
|
||||
*/
|
||||
private int delay;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
|
||||
package cn.orionsec.ops.handler.tail;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TailSessionHolder {
|
||||
|
||||
/**
|
||||
* key: token
|
||||
* value: ITailHandler
|
||||
*/
|
||||
private final Map<String, ITailHandler> holder = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* key: machineId:filePath
|
||||
* value: token
|
||||
*/
|
||||
private final Map<String, List<String>> fileTokenMapping = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* 添加session
|
||||
*
|
||||
* @param token token
|
||||
* @param session session
|
||||
*/
|
||||
public void addSession(String token, ITailHandler session) {
|
||||
holder.put(token, session);
|
||||
fileTokenMapping.computeIfAbsent(session.getMachineId() + ":" + session.getFilePath(), s -> Lists.newList()).add(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取session
|
||||
*
|
||||
* @param token token
|
||||
* @return session
|
||||
*/
|
||||
public ITailHandler getSession(String token) {
|
||||
return holder.get(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取session
|
||||
*
|
||||
* @param machineId machineId
|
||||
* @param path path
|
||||
* @return session
|
||||
*/
|
||||
public List<ITailHandler> getSession(Long machineId, String path) {
|
||||
List<String> tokenList = fileTokenMapping.get(machineId + ":" + path);
|
||||
if (Lists.isEmpty(tokenList)) {
|
||||
return Lists.empty();
|
||||
}
|
||||
return tokenList.stream()
|
||||
.map(holder::get)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除session
|
||||
*
|
||||
* @param token token
|
||||
* @return session
|
||||
*/
|
||||
public ITailHandler removeSession(String token) {
|
||||
ITailHandler handler = holder.remove(token);
|
||||
if (handler != null) {
|
||||
fileTokenMapping.remove(handler.getMachineId() + ":" + handler.getFilePath());
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步关闭进行中的 tail
|
||||
*
|
||||
* @param machineId machineId
|
||||
* @param path path
|
||||
*/
|
||||
public void asyncCloseTailFile(Long machineId, String path) {
|
||||
Threads.start(() -> {
|
||||
try {
|
||||
Threads.sleep(Const.MS_S_1);
|
||||
this.getSession(machineId, path).forEach(ITailHandler::setLastModify);
|
||||
Threads.sleep(Const.MS_S_5);
|
||||
this.getSession(machineId, path).forEach(ITailHandler::close);
|
||||
} catch (Exception e) {
|
||||
log.error("关闭tailingFile失败 machineId: {}, path: {}", machineId, path, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
|
||||
package cn.orionsec.ops.handler.tail.impl;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.ssh.command.CommandExecutor;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.handler.tail.ITailHandler;
|
||||
import cn.orionsec.ops.handler.tail.TailFileHint;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.BinaryMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@Slf4j
|
||||
public class ExecTailFileHandler implements ITailHandler {
|
||||
|
||||
protected static MachineInfoService machineInfoService = SpringHolder.getBean(MachineInfoService.class);
|
||||
|
||||
@Getter
|
||||
private final String token;
|
||||
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
private final WebSocketSession session;
|
||||
|
||||
/**
|
||||
* hint
|
||||
*/
|
||||
private final TailFileHint hint;
|
||||
|
||||
private SessionStore sessionStore;
|
||||
|
||||
private CommandExecutor executor;
|
||||
|
||||
private volatile boolean close;
|
||||
|
||||
public ExecTailFileHandler(TailFileHint hint, WebSocketSession session) {
|
||||
this.token = hint.getToken();
|
||||
this.hint = hint;
|
||||
this.session = session;
|
||||
log.info("tail EXEC_TAIL 监听文件初始化 token: {}, hint: {}", token, JSON.toJSONString(hint));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
try {
|
||||
// 打开session
|
||||
this.sessionStore = machineInfoService.openSessionStore(hint.getMachineId());
|
||||
log.info("tail 建立连接成功 machineId: {}", hint.getMachineId());
|
||||
} catch (Exception e) {
|
||||
WebSockets.openSessionStoreThrowClose(session, e);
|
||||
log.error("tail 建立连接失败-连接远程服务器失败 e: {}, machineId: {}", e, hint.getMachineId());
|
||||
return;
|
||||
}
|
||||
// 打开 command
|
||||
this.executor = sessionStore.getCommandExecutor(Strings.replaceCRLF(hint.getCommand()));
|
||||
executor.merge();
|
||||
executor.callback(this::callback);
|
||||
executor.streamHandler(this::handler);
|
||||
executor.connect();
|
||||
SchedulerPools.TAIL_SCHEDULER.execute(executor);
|
||||
log.info("tail EXEC_TAIL 监听文件开始 token: {}", token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String command) {
|
||||
executor.write(command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getMachineId() {
|
||||
return hint.getMachineId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilePath() {
|
||||
return hint.getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调
|
||||
*/
|
||||
private void callback() {
|
||||
log.info("tail EXEC_TAIL 监听文件结束 token: {}", token);
|
||||
WebSockets.close(session, WsCloseCode.EOF);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理标准输入流
|
||||
*
|
||||
* @param input 流
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void handler(InputStream input) {
|
||||
byte[] buffer = new byte[Const.BUFFER_KB_8];
|
||||
int read;
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(new BinaryMessage(buffer, 0, read, true));
|
||||
}
|
||||
}
|
||||
|
||||
// 2.0 tail 换行有问题
|
||||
// BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, hint.getCharset()), Const.BUFFER_KB_8);
|
||||
// String line;
|
||||
// while ((line = reader.readLine()) != null) {
|
||||
// session.sendMessage(new TextMessage(line));
|
||||
// }
|
||||
|
||||
// 1.0 tracker 换行有问题
|
||||
// char[] buffer = new char[Const.BUFFER_KB_4];
|
||||
// int read;
|
||||
// StringBuilder sb = new StringBuilder();
|
||||
// // tail命令结合BufferedReader对CR处理有问题 所以不能使用readLine
|
||||
// while ((read = reader.read(buffer)) != -1) {
|
||||
// int mark = -1;
|
||||
// for (int i = 0; i < read; i++) {
|
||||
// // 读取到行结尾
|
||||
// if (buffer[i] == Letters.LF) {
|
||||
// sb.append(buffer, mark + 1, i - mark - 1);
|
||||
// if (session.isOpen()) {
|
||||
// String payload = sb.toString().replaceAll(Const.CR, Const.EMPTY);
|
||||
// session.sendMessage(new TextMessage(payload));
|
||||
// }
|
||||
// sb = new StringBuilder();
|
||||
// mark = i;
|
||||
// }
|
||||
// }
|
||||
// // 不是完整的一行
|
||||
// if (mark == -1) {
|
||||
// sb.append(buffer, 0, read);
|
||||
// } else if (mark != read - 1) {
|
||||
// sb.append(buffer, mark + 1, read - mark - 1);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void close() {
|
||||
if (close) {
|
||||
return;
|
||||
}
|
||||
this.close = true;
|
||||
Streams.close(executor);
|
||||
Streams.close(sessionStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return hint.getPath();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
|
||||
package cn.orionsec.ops.handler.tail.impl;
|
||||
|
||||
import cn.orionsec.kit.ext.tail.Tracker;
|
||||
import cn.orionsec.kit.ext.tail.delay.DelayTrackerListener;
|
||||
import cn.orionsec.kit.ext.tail.handler.DataHandler;
|
||||
import cn.orionsec.kit.ext.tail.mode.FileNotFoundMode;
|
||||
import cn.orionsec.kit.ext.tail.mode.FileOffsetMode;
|
||||
import cn.orionsec.kit.lang.define.thread.HookRunnable;
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.handler.tail.ITailHandler;
|
||||
import cn.orionsec.ops.handler.tail.TailFileHint;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.BinaryMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
@Slf4j
|
||||
public class TrackerTailFileHandler implements ITailHandler, DataHandler {
|
||||
|
||||
@Getter
|
||||
private final String token;
|
||||
|
||||
/**
|
||||
* session
|
||||
*/
|
||||
private final WebSocketSession session;
|
||||
|
||||
/**
|
||||
* hint
|
||||
*/
|
||||
private final TailFileHint hint;
|
||||
|
||||
private DelayTrackerListener tracker;
|
||||
|
||||
private volatile boolean close;
|
||||
|
||||
@Getter
|
||||
private final String filePath;
|
||||
|
||||
public TrackerTailFileHandler(TailFileHint hint, WebSocketSession session) {
|
||||
this.token = hint.getToken();
|
||||
this.hint = hint;
|
||||
this.filePath = hint.getPath();
|
||||
this.session = session;
|
||||
log.info("tail TRACKER 监听文件初始化 token: {}, hint: {}", token, JSON.toJSONString(hint));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.tracker = new DelayTrackerListener(filePath, this);
|
||||
tracker.delayMillis(hint.getDelay());
|
||||
tracker.charset(hint.getCharset());
|
||||
tracker.offset(FileOffsetMode.LINE, hint.getOffset());
|
||||
tracker.notFoundMode(FileNotFoundMode.WAIT_COUNT, 10);
|
||||
Threads.start(new HookRunnable(() -> {
|
||||
log.info("tail TRACKER 开始监听文件 token: {}", token);
|
||||
tracker.tail();
|
||||
}, this::callback), SchedulerPools.TAIL_SCHEDULER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastModify() {
|
||||
tracker.setFileLastModifyTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getMachineId() {
|
||||
return Const.HOST_MACHINE_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void callback() {
|
||||
log.info("tail TRACKER 监听文件结束 token: {}", token);
|
||||
WebSockets.close(session, WsCloseCode.EOF);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void read(byte[] bytes, int len, Tracker tracker) {
|
||||
if (session.isOpen()) {
|
||||
session.sendMessage(new BinaryMessage(bytes, 0, len, true));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void close() {
|
||||
if (close) {
|
||||
return;
|
||||
}
|
||||
this.close = true;
|
||||
if (tracker != null) {
|
||||
tracker.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
|
||||
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
|
||||
import cn.orionsec.ops.handler.terminal.manager.TerminalManagementHandler;
|
||||
import cn.orionsec.ops.handler.terminal.watcher.ITerminalWatcherProcessor;
|
||||
|
||||
public interface IOperateHandler extends TerminalManagementHandler, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 建立连接
|
||||
*/
|
||||
void connect();
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
void disconnect();
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param operate 操作
|
||||
* @param body body
|
||||
* @throws Exception ex
|
||||
*/
|
||||
void handleMessage(TerminalClientOperate operate, String body) throws Exception;
|
||||
|
||||
/**
|
||||
* 心跳是否结束
|
||||
*
|
||||
* @return true结束
|
||||
*/
|
||||
boolean isDown();
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
*
|
||||
* @return token
|
||||
*/
|
||||
String getToken();
|
||||
|
||||
/**
|
||||
* 获取终端配置
|
||||
*
|
||||
* @return 终端配置
|
||||
*/
|
||||
TerminalConnectConfig getHint();
|
||||
|
||||
/**
|
||||
* 获取监视器
|
||||
*
|
||||
* @return processor
|
||||
*/
|
||||
ITerminalWatcherProcessor getWatcher();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal;
|
||||
|
||||
import cn.orionsec.kit.lang.define.wrapper.Tuple;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.constant.ws.WsProtocol;
|
||||
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
|
||||
import cn.orionsec.ops.entity.domain.MachineInfoDO;
|
||||
import cn.orionsec.ops.entity.domain.MachineTerminalLogDO;
|
||||
import cn.orionsec.ops.entity.dto.terminal.TerminalConnectDTO;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.handler.terminal.manager.TerminalSessionManager;
|
||||
import cn.orionsec.ops.service.api.MachineInfoService;
|
||||
import cn.orionsec.ops.service.api.MachineTerminalService;
|
||||
import cn.orionsec.ops.service.api.PassportService;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Component("terminalMessageHandler")
|
||||
public class TerminalMessageHandler implements WebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private TerminalSessionManager terminalSessionManager;
|
||||
|
||||
@Resource
|
||||
private PassportService passportService;
|
||||
|
||||
@Resource
|
||||
private MachineInfoService machineInfoService;
|
||||
|
||||
@Resource
|
||||
private MachineTerminalService machineTerminalService;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
log.info("terminal 已建立连接 token: {}, id: {}, params: {}", WebSockets.getToken(session), session.getId(), session.getAttributes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
if (!(message instanceof TextMessage)) {
|
||||
return;
|
||||
}
|
||||
String token = session.getId();
|
||||
try {
|
||||
// 解析请求
|
||||
Tuple tuple = WebSockets.parsePayload(((TextMessage) message).getPayload());
|
||||
if (tuple == null) {
|
||||
WebSockets.sendText(session, WsProtocol.ERROR.get());
|
||||
return;
|
||||
}
|
||||
TerminalClientOperate operate = tuple.get(0);
|
||||
String body = tuple.get(1);
|
||||
|
||||
// 建立连接
|
||||
if (operate == TerminalClientOperate.CONNECT) {
|
||||
// 建立连接
|
||||
if (session.getAttributes().get(WebSockets.CONNECTED) != null) {
|
||||
return;
|
||||
}
|
||||
this.connect(session, token, body);
|
||||
return;
|
||||
}
|
||||
// 检查连接
|
||||
if (session.getAttributes().get(WebSockets.CONNECTED) == null) {
|
||||
WebSockets.close(session, WsCloseCode.VALID);
|
||||
return;
|
||||
}
|
||||
// 获取连接
|
||||
IOperateHandler handler = terminalSessionManager.getSession(token);
|
||||
if (handler == null) {
|
||||
WebSockets.close(session, WsCloseCode.UNKNOWN_CONNECT);
|
||||
return;
|
||||
}
|
||||
// 操作
|
||||
handler.handleMessage(operate, body);
|
||||
} catch (Exception e) {
|
||||
log.error("terminal 处理操作异常 token: {}", token, e);
|
||||
WebSockets.close(session, WsCloseCode.RUNTIME_EXCEPTION);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||
log.error("terminal 操作异常拦截 token: {}", session.getId(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
String token = session.getId();
|
||||
int code = status.getCode();
|
||||
log.info("terminal 关闭连接 token: {}, code: {}, reason: {}", token, code, status.getReason());
|
||||
// 释放资源
|
||||
IOperateHandler handler = terminalSessionManager.removeSession(token);
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
handler.close();
|
||||
// 修改日志
|
||||
MachineTerminalLogDO updateLog = new MachineTerminalLogDO();
|
||||
updateLog.setCloseCode(code);
|
||||
updateLog.setDisconnectedTime(new Date());
|
||||
Integer effect = machineTerminalService.updateAccessLog(token, updateLog);
|
||||
log.info("terminal 连接关闭更新日志 token: {}, effect: {}", token, effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPartialMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立连接
|
||||
*
|
||||
* @param session session
|
||||
* @param token token
|
||||
* @param body body
|
||||
*/
|
||||
private void connect(WebSocketSession session, String token, String body) {
|
||||
log.info("terminal 尝试建立连接 token: {}, body: {}", token, body);
|
||||
// 检查参数
|
||||
TerminalConnectDTO connectInfo = TerminalUtils.parseConnectBody(body);
|
||||
if (connectInfo == null) {
|
||||
WebSockets.sendText(session, WsProtocol.ERROR.get());
|
||||
return;
|
||||
}
|
||||
Long userId = (Long) session.getAttributes().get(WebSockets.UID);
|
||||
Long machineId = (Long) session.getAttributes().get(WebSockets.MID);
|
||||
|
||||
// 获取登录用户
|
||||
UserDTO userDTO = passportService.getUserByToken(connectInfo.getLoginToken(), null);
|
||||
if (userDTO == null || !userId.equals(userDTO.getId())) {
|
||||
log.info("terminal 建立连接拒绝-用户认证失败 token: {}", token);
|
||||
WebSockets.close(session, WsCloseCode.IDENTITY_MISMATCH);
|
||||
return;
|
||||
}
|
||||
// 获取机器信息
|
||||
MachineInfoDO machine = machineInfoService.selectById(machineId);
|
||||
if (machine == null) {
|
||||
log.info("terminal 建立连接拒绝-未查询到机器信息 token: {}, machineId: {}", token, machineId);
|
||||
WebSockets.close(session, WsCloseCode.INVALID_MACHINE);
|
||||
return;
|
||||
}
|
||||
session.getAttributes().put(WebSockets.CONNECTED, 1);
|
||||
// 建立连接
|
||||
SessionStore sessionStore;
|
||||
try {
|
||||
// 打开session
|
||||
sessionStore = machineInfoService.openSessionStore(machine);
|
||||
WebSockets.sendText(session, WsProtocol.CONNECTED.get());
|
||||
} catch (Exception e) {
|
||||
WebSockets.openSessionStoreThrowClose(session, e);
|
||||
log.error("terminal 建立连接失败-连接远程服务器失败 uid: {}, machineId: {}", userId, machineId, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置
|
||||
TerminalConnectConfig hint = new TerminalConnectConfig();
|
||||
String terminalType = machineTerminalService.getMachineConfig(machineId).getTerminalType();
|
||||
hint.setUserId(userId);
|
||||
hint.setUsername(userDTO.getUsername());
|
||||
hint.setMachineId(machineId);
|
||||
hint.setMachineName(machine.getMachineName());
|
||||
hint.setMachineHost(machine.getMachineHost());
|
||||
hint.setMachineTag(machine.getMachineTag());
|
||||
hint.setCols(connectInfo.getCols());
|
||||
hint.setRows(connectInfo.getRows());
|
||||
hint.setTerminalType(terminalType);
|
||||
TerminalOperateHandler terminalHandler = new TerminalOperateHandler(token, hint, session, sessionStore);
|
||||
try {
|
||||
// 打开shell
|
||||
log.info("terminal 尝试建立连接-尝试打开shell token: {}", token);
|
||||
terminalHandler.connect();
|
||||
log.info("terminal 建立连接成功-打开shell成功 token: {}", token);
|
||||
} catch (Exception e) {
|
||||
WebSockets.close(session, WsCloseCode.OPEN_SHELL_EXCEPTION);
|
||||
log.error("terminal 建立连接失败-打开shell失败 machineId: {}, uid: {}", machineId, userId, e);
|
||||
return;
|
||||
}
|
||||
terminalSessionManager.addSession(token, terminalHandler);
|
||||
log.info("terminal 建立连接成功 uid: {}, machineId: {}", userId, machineId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.Letters;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.io.Files1;
|
||||
import cn.orionsec.kit.lang.utils.io.Streams;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.net.host.SessionStore;
|
||||
import cn.orionsec.kit.net.host.ssh.shell.ShellExecutor;
|
||||
import cn.orionsec.kit.spring.SpringHolder;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
|
||||
import cn.orionsec.ops.constant.terminal.TerminalConst;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.constant.ws.WsProtocol;
|
||||
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
|
||||
import cn.orionsec.ops.entity.domain.MachineTerminalLogDO;
|
||||
import cn.orionsec.ops.entity.dto.terminal.TerminalSizeDTO;
|
||||
import cn.orionsec.ops.handler.terminal.screen.TerminalScreenEnv;
|
||||
import cn.orionsec.ops.handler.terminal.screen.TerminalScreenHeader;
|
||||
import cn.orionsec.ops.handler.terminal.watcher.ITerminalWatcherProcessor;
|
||||
import cn.orionsec.ops.handler.terminal.watcher.TerminalWatcherProcessor;
|
||||
import cn.orionsec.ops.service.api.MachineTerminalService;
|
||||
import cn.orionsec.ops.utils.PathBuilders;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
public class TerminalOperateHandler implements IOperateHandler {
|
||||
|
||||
private static final MachineTerminalService machineTerminalService = SpringHolder.getBean(MachineTerminalService.class);
|
||||
|
||||
private static final String SCREEN_BODY_TEMPLATE = "[{}, \"o\", \"{}\"]";
|
||||
|
||||
@Getter
|
||||
private final String token;
|
||||
|
||||
@Getter
|
||||
private final TerminalConnectConfig hint;
|
||||
|
||||
@Getter
|
||||
private final ITerminalWatcherProcessor watcher;
|
||||
|
||||
private final WebSocketSession session;
|
||||
|
||||
private final SessionStore sessionStore;
|
||||
|
||||
private ShellExecutor executor;
|
||||
|
||||
private long connectedTime;
|
||||
|
||||
/**
|
||||
* 录屏流
|
||||
*/
|
||||
private OutputStream screenStream;
|
||||
|
||||
/**
|
||||
* 最后一次心跳通讯时间
|
||||
*/
|
||||
private volatile long lastHeartbeat;
|
||||
|
||||
protected volatile boolean close;
|
||||
|
||||
public TerminalOperateHandler(String token, TerminalConnectConfig hint, WebSocketSession session, SessionStore sessionStore) {
|
||||
this.token = token;
|
||||
this.hint = hint;
|
||||
this.watcher = new TerminalWatcherProcessor();
|
||||
this.session = session;
|
||||
this.sessionStore = sessionStore;
|
||||
this.lastHeartbeat = System.currentTimeMillis();
|
||||
this.initShell();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
executor.connect();
|
||||
executor.streamHandler(this::streamHandler);
|
||||
// 连接成功后初始化日志信息
|
||||
this.initLog();
|
||||
// 开始监听输出
|
||||
SchedulerPools.TERMINAL_SCHEDULER.execute(executor);
|
||||
watcher.watch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 shell
|
||||
*/
|
||||
private void initShell() {
|
||||
// 初始化 shell 执行器
|
||||
this.executor = sessionStore.getShellExecutor();
|
||||
executor.terminalType(hint.getTerminalType());
|
||||
executor.size(hint.getCols(), hint.getRows());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化日志
|
||||
*/
|
||||
private void initLog() {
|
||||
this.connectedTime = System.currentTimeMillis();
|
||||
hint.setConnectedTime(new Date(connectedTime));
|
||||
// 初始化录屏
|
||||
String screenPath = this.initScreenStream();
|
||||
log.info("terminal 开始记录用户操作录屏: {} {}", token, screenPath);
|
||||
// 记录日志
|
||||
MachineTerminalLogDO logEntity = new MachineTerminalLogDO();
|
||||
logEntity.setAccessToken(token);
|
||||
logEntity.setUserId(hint.getUserId());
|
||||
logEntity.setUsername(hint.getUsername());
|
||||
logEntity.setMachineId(hint.getMachineId());
|
||||
logEntity.setMachineName(hint.getMachineName());
|
||||
logEntity.setMachineTag(hint.getMachineTag());
|
||||
logEntity.setMachineHost(hint.getMachineHost());
|
||||
logEntity.setConnectedTime(hint.getConnectedTime());
|
||||
logEntity.setScreenPath(screenPath);
|
||||
Long logId = machineTerminalService.addTerminalLog(logEntity);
|
||||
hint.setLogId(logId);
|
||||
log.info("terminal 保存用户操作日志: {} logId: {}", token, logId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化录屏流
|
||||
*
|
||||
* @return path
|
||||
*/
|
||||
@SneakyThrows
|
||||
private String initScreenStream() {
|
||||
// 初始化流
|
||||
String screenPath = PathBuilders.getTerminalScreenPath(hint.getUserId(), hint.getMachineId());
|
||||
String realScreenPath = Files1.getPath(SystemEnvAttr.SCREEN_PATH.getValue(), screenPath);
|
||||
this.screenStream = Files1.openOutputStreamFastSafe(realScreenPath);
|
||||
// 设置头
|
||||
TerminalScreenHeader header = new TerminalScreenHeader();
|
||||
String title = Strings.format("{}({}) {} {}", hint.getMachineName(), hint.getMachineHost(),
|
||||
hint.getUsername(), Dates.format(hint.getConnectedTime()));
|
||||
header.setTitle(title);
|
||||
header.setCols(hint.getCols());
|
||||
header.setRows(hint.getRows());
|
||||
header.setTimestamp(connectedTime / Dates.SECOND_STAMP);
|
||||
header.setEnv(new TerminalScreenEnv(hint.getTerminalType()));
|
||||
// 拼接头
|
||||
screenStream.write(JSON.toJSONBytes(header));
|
||||
screenStream.write(Letters.LF);
|
||||
screenStream.flush();
|
||||
return screenPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准输出处理
|
||||
*
|
||||
* @param inputStream stream
|
||||
*/
|
||||
private void streamHandler(InputStream inputStream) {
|
||||
byte[] bs = new byte[Const.BUFFER_KB_4];
|
||||
BufferedInputStream in = new BufferedInputStream(inputStream, Const.BUFFER_KB_4);
|
||||
int read;
|
||||
try {
|
||||
while (session.isOpen() && (read = in.read(bs)) != -1) {
|
||||
// 响应
|
||||
byte[] msg = WsProtocol.OK.msg(bs, 0, read);
|
||||
WebSockets.sendText(session, msg);
|
||||
// 响应监视
|
||||
watcher.sendMessage(msg);
|
||||
// 记录录屏
|
||||
String row = Strings.format(SCREEN_BODY_TEMPLATE,
|
||||
((double) (System.currentTimeMillis() - connectedTime)) / Dates.SECOND_STAMP,
|
||||
Utils.convertControlUnicode(new String(bs, 0, read)));
|
||||
screenStream.write(Strings.bytes(row));
|
||||
screenStream.write(Letters.LF);
|
||||
screenStream.flush();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
log.error("terminal 读取流失败", ex);
|
||||
WebSockets.close(session, WsCloseCode.READ_EXCEPTION);
|
||||
}
|
||||
// eof
|
||||
if (close) {
|
||||
return;
|
||||
}
|
||||
WebSockets.close(session, WsCloseCode.EOF);
|
||||
log.info("terminal eof回调 {}", token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
if (close) {
|
||||
return;
|
||||
}
|
||||
this.close = true;
|
||||
try {
|
||||
Streams.close(screenStream);
|
||||
Streams.close(executor);
|
||||
Streams.close(sessionStore);
|
||||
} catch (Exception e) {
|
||||
log.error("terminal 断开连接 失败 token: {}", token, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forcedOffline() {
|
||||
WebSockets.close(session, WsCloseCode.FORCED_OFFLINE);
|
||||
log.info("terminal 管理员强制断连 {}", token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void heartbeatDownClose() {
|
||||
WebSockets.close(session, WsCloseCode.HEART_DOWN);
|
||||
log.info("terminal 心跳结束断连 {}", token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendHeartbeat() {
|
||||
WebSockets.sendText(session, WsProtocol.PING.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDown() {
|
||||
return (System.currentTimeMillis() - lastHeartbeat) > TerminalConst.TERMINAL_CONNECT_DOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(TerminalClientOperate operate, String body) {
|
||||
if (close) {
|
||||
return;
|
||||
}
|
||||
switch (operate) {
|
||||
case KEY:
|
||||
executor.write(Strings.bytes(body));
|
||||
return;
|
||||
case PING:
|
||||
// client 主动发送 ping
|
||||
this.lastHeartbeat = System.currentTimeMillis();
|
||||
WebSockets.sendText(session, WsProtocol.PONG.get());
|
||||
return;
|
||||
case PONG:
|
||||
// server 主动发送 ping, client 响应 pong
|
||||
this.lastHeartbeat = System.currentTimeMillis();
|
||||
return;
|
||||
case RESIZE:
|
||||
this.resize(body);
|
||||
return;
|
||||
case COMMAND:
|
||||
executor.write(Strings.bytes(body));
|
||||
executor.write(new byte[]{Letters.LF});
|
||||
case CLEAR:
|
||||
executor.write(new byte[]{12});
|
||||
return;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.disconnect();
|
||||
Streams.close(watcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置大小
|
||||
*/
|
||||
private void resize(String body) {
|
||||
// 检查参数
|
||||
TerminalSizeDTO window = TerminalUtils.parseResizeBody(body);
|
||||
if (window == null) {
|
||||
WebSockets.sendText(session, WsProtocol.ERROR.get());
|
||||
return;
|
||||
}
|
||||
hint.setCols(window.getCols());
|
||||
hint.setRows(window.getRows());
|
||||
if (!executor.isConnected()) {
|
||||
executor.connect();
|
||||
}
|
||||
executor.size(window.getCols(), window.getRows());
|
||||
executor.resize();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.ops.entity.dto.terminal.TerminalConnectDTO;
|
||||
import cn.orionsec.ops.entity.dto.terminal.TerminalSizeDTO;
|
||||
|
||||
public class TerminalUtils {
|
||||
|
||||
private TerminalUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析连接参数
|
||||
* <p>
|
||||
* .e.g cols|rows|loginToken
|
||||
*
|
||||
* @param body body
|
||||
* @return connect
|
||||
*/
|
||||
public static TerminalConnectDTO parseConnectBody(String body) {
|
||||
String[] arr = body.split("\\|");
|
||||
if (arr.length != 3) {
|
||||
return null;
|
||||
}
|
||||
// 解析 size
|
||||
if (!Strings.isInteger(arr[0]) || !Strings.isInteger(arr[1])) {
|
||||
return null;
|
||||
}
|
||||
TerminalConnectDTO connect = new TerminalConnectDTO();
|
||||
connect.setCols(Integer.parseInt(arr[0]));
|
||||
connect.setRows(Integer.parseInt(arr[1]));
|
||||
connect.setLoginToken(arr[2]);
|
||||
return connect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析修改大小参数
|
||||
* <p>
|
||||
* .e.g cols|rows
|
||||
*
|
||||
* @param body body
|
||||
* @return size
|
||||
*/
|
||||
public static TerminalSizeDTO parseResizeBody(String body) {
|
||||
String[] arr = body.split("\\|");
|
||||
if (arr.length != 2) {
|
||||
return null;
|
||||
}
|
||||
// 解析 size
|
||||
if (!Strings.isInteger(arr[0]) || !Strings.isInteger(arr[1])) {
|
||||
return null;
|
||||
}
|
||||
TerminalSizeDTO size = new TerminalSizeDTO();
|
||||
size.setCols(Integer.parseInt(arr[0]));
|
||||
size.setRows(Integer.parseInt(arr[1]));
|
||||
return size;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.manager;
|
||||
|
||||
public interface TerminalManagementHandler {
|
||||
|
||||
/**
|
||||
* 管理员强制下线
|
||||
*
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
void forcedOffline() throws Exception;
|
||||
|
||||
/**
|
||||
* 心跳结束下线
|
||||
*
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
void heartbeatDownClose() throws Exception;
|
||||
|
||||
/**
|
||||
* 主动发送心跳
|
||||
*/
|
||||
void sendHeartbeat();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.manager;
|
||||
|
||||
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
|
||||
import cn.orionsec.kit.lang.id.UUIds;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.Valid;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.kit.lang.utils.convert.Converts;
|
||||
import cn.orionsec.kit.lang.utils.time.DateRanges;
|
||||
import cn.orionsec.ops.constant.KeyConst;
|
||||
import cn.orionsec.ops.constant.MessageConst;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.entity.config.TerminalConnectConfig;
|
||||
import cn.orionsec.ops.entity.dto.terminal.TerminalWatcherDTO;
|
||||
import cn.orionsec.ops.entity.request.machine.MachineTerminalManagerRequest;
|
||||
import cn.orionsec.ops.entity.vo.machine.MachineTerminalManagerVO;
|
||||
import cn.orionsec.ops.entity.vo.machine.TerminalWatcherVO;
|
||||
import cn.orionsec.ops.handler.terminal.IOperateHandler;
|
||||
import cn.orionsec.ops.utils.Currents;
|
||||
import cn.orionsec.ops.utils.EventParamsHolder;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class TerminalSessionManager {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
/**
|
||||
* 会话 sessionId:handler
|
||||
*/
|
||||
private final Map<String, IOperateHandler> sessionHolder = Maps.newCurrentHashMap();
|
||||
|
||||
/**
|
||||
* session 列表
|
||||
*
|
||||
* @param request request
|
||||
* @return dataGrid
|
||||
*/
|
||||
public DataGrid<MachineTerminalManagerVO> getOnlineTerminal(MachineTerminalManagerRequest request) {
|
||||
List<MachineTerminalManagerVO> sessionList = sessionHolder.values()
|
||||
.stream()
|
||||
.filter(s -> Optional.ofNullable(request.getToken())
|
||||
.filter(Strings::isNotBlank)
|
||||
.map(t -> s.getToken().contains(t))
|
||||
.orElse(true))
|
||||
.filter(s -> Optional.ofNullable(request.getMachineName())
|
||||
.filter(Strings::isNotBlank)
|
||||
.map(t -> s.getHint().getMachineName().toLowerCase().contains(t.toLowerCase()))
|
||||
.orElse(true))
|
||||
.filter(s -> Optional.ofNullable(request.getMachineTag())
|
||||
.filter(Strings::isNotBlank)
|
||||
.map(t -> s.getHint().getMachineTag().contains(t))
|
||||
.orElse(true))
|
||||
.filter(s -> Optional.ofNullable(request.getMachineHost())
|
||||
.filter(Strings::isNotBlank)
|
||||
.map(t -> s.getHint().getMachineHost().contains(t))
|
||||
.orElse(true))
|
||||
.filter(s -> Optional.ofNullable(request.getUserId())
|
||||
.map(t -> s.getHint().getUserId().equals(t))
|
||||
.orElse(true))
|
||||
.filter(s -> Optional.ofNullable(request.getUsername())
|
||||
.map(t -> s.getHint().getUsername().toLowerCase().contains(t.toLowerCase()))
|
||||
.orElse(true))
|
||||
.filter(s -> Optional.ofNullable(request.getMachineId())
|
||||
.map(t -> s.getHint().getMachineId().equals(t))
|
||||
.orElse(true))
|
||||
.filter(s -> {
|
||||
if (request.getConnectedTimeStart() == null || request.getConnectedTimeEnd() == null) {
|
||||
return true;
|
||||
}
|
||||
return DateRanges.inRange(request.getConnectedTimeStart(), request.getConnectedTimeEnd(), s.getHint().getConnectedTime());
|
||||
})
|
||||
.map(s -> {
|
||||
MachineTerminalManagerVO vo = Converts.to(s.getHint(), MachineTerminalManagerVO.class);
|
||||
vo.setToken(s.getToken());
|
||||
return vo;
|
||||
})
|
||||
.sorted(Comparator.comparing(MachineTerminalManagerVO::getConnectedTime).reversed())
|
||||
.collect(Collectors.toList());
|
||||
List<MachineTerminalManagerVO> page = Lists.newLimitList(sessionList)
|
||||
.limit(request.getLimit())
|
||||
.page(request.getPage());
|
||||
return DataGrid.of(page, sessionList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制下线
|
||||
*
|
||||
* @param token token
|
||||
*/
|
||||
public void forceOffline(String token) {
|
||||
IOperateHandler handler = sessionHolder.get(token);
|
||||
Valid.notNull(handler, MessageConst.SESSION_PRESENT);
|
||||
try {
|
||||
// 下线
|
||||
handler.forcedOffline();
|
||||
// 设置日志参数
|
||||
TerminalConnectConfig hint = handler.getHint();
|
||||
EventParamsHolder.addParam(EventKeys.TOKEN, token);
|
||||
EventParamsHolder.addParam(EventKeys.USERNAME, hint.getUsername());
|
||||
EventParamsHolder.addParam(EventKeys.NAME, hint.getMachineName());
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.app(MessageConst.OPERATOR_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取终端监视 token
|
||||
*
|
||||
* @param token token
|
||||
* @param readonly readonly
|
||||
* @return watcher
|
||||
*/
|
||||
public TerminalWatcherVO getWatcherToken(String token, Integer readonly) {
|
||||
IOperateHandler handler = sessionHolder.get(token);
|
||||
Valid.notNull(handler, MessageConst.SESSION_PRESENT);
|
||||
// 设置缓存
|
||||
String watcherToken = UUIds.random32();
|
||||
TerminalWatcherDTO cache = TerminalWatcherDTO.builder()
|
||||
.userId(Currents.getUserId())
|
||||
.token(token)
|
||||
.readonly(readonly)
|
||||
.build();
|
||||
String key = Strings.format(KeyConst.TERMINAL_WATCHER_TOKEN, watcherToken);
|
||||
redisTemplate.opsForValue().set(key, JSON.toJSONString(cache),
|
||||
KeyConst.TERMINAL_WATCHER_TOKEN_EXPIRE, TimeUnit.SECONDS);
|
||||
// 设置返回
|
||||
TerminalConnectConfig hint = handler.getHint();
|
||||
return TerminalWatcherVO.builder()
|
||||
.token(watcherToken)
|
||||
.readonly(readonly)
|
||||
.cols(hint.getCols())
|
||||
.rows(hint.getRows())
|
||||
.build();
|
||||
}
|
||||
|
||||
public Map<String, IOperateHandler> getSessionHolder() {
|
||||
return sessionHolder;
|
||||
}
|
||||
|
||||
public IOperateHandler getSession(String key) {
|
||||
return sessionHolder.get(key);
|
||||
}
|
||||
|
||||
public IOperateHandler removeSession(String key) {
|
||||
return sessionHolder.remove(key);
|
||||
}
|
||||
|
||||
public void addSession(String key, IOperateHandler handler) {
|
||||
sessionHolder.put(key, handler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.screen;
|
||||
|
||||
import cn.orionsec.kit.net.host.ssh.TerminalType;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TerminalScreenEnv {
|
||||
|
||||
/**
|
||||
* 终端类型
|
||||
*/
|
||||
@JSONField(name = "TERM")
|
||||
private String term;
|
||||
|
||||
/**
|
||||
* shell 类型
|
||||
*/
|
||||
@JSONField(name = "SHELL")
|
||||
private String shell;
|
||||
|
||||
public TerminalScreenEnv() {
|
||||
this(TerminalType.XTERM.getType(), Const.DEFAULT_SHELL);
|
||||
}
|
||||
|
||||
public TerminalScreenEnv(String term) {
|
||||
this(term, Const.DEFAULT_SHELL);
|
||||
}
|
||||
|
||||
public TerminalScreenEnv(String term, String shell) {
|
||||
this.term = term;
|
||||
this.shell = shell;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.screen;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TerminalScreenHeader {
|
||||
|
||||
private static final Integer CAST_VERSION = 2;
|
||||
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* cols
|
||||
*/
|
||||
@JSONField(name = "width")
|
||||
private Integer cols;
|
||||
|
||||
/**
|
||||
* rows
|
||||
*/
|
||||
@JSONField(name = "height")
|
||||
private Integer rows;
|
||||
|
||||
/**
|
||||
* 开始时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 环境变量
|
||||
*/
|
||||
private TerminalScreenEnv env;
|
||||
|
||||
public TerminalScreenHeader() {
|
||||
this.version = CAST_VERSION;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.watcher;
|
||||
|
||||
import cn.orionsec.kit.lang.able.SafeCloseable;
|
||||
import cn.orionsec.kit.lang.able.Watchable;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
public interface ITerminalWatcherProcessor extends Watchable, SafeCloseable {
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param message message
|
||||
*/
|
||||
void sendMessage(byte[] message);
|
||||
|
||||
/**
|
||||
* 添加 watcher
|
||||
*
|
||||
* @param session session
|
||||
*/
|
||||
void addWatcher(WebSocketSession session);
|
||||
|
||||
/**
|
||||
* 移除 watcher
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
void removeWatcher(String id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.watcher;
|
||||
|
||||
import cn.orionsec.kit.lang.define.wrapper.Tuple;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.terminal.TerminalClientOperate;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.constant.ws.WsProtocol;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.handler.terminal.IOperateHandler;
|
||||
import cn.orionsec.ops.handler.terminal.manager.TerminalSessionManager;
|
||||
import cn.orionsec.ops.service.api.PassportService;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Slf4j
|
||||
@Component("terminalWatcherHandler")
|
||||
public class TerminalWatcherHandler implements WebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private TerminalSessionManager terminalSessionManager;
|
||||
|
||||
@Resource
|
||||
private PassportService passportService;
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
log.info("terminal-watcher 已建立连接 token: {}, id: {}, params: {}", WebSockets.getToken(session), session.getId(), session.getAttributes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
if (!(message instanceof TextMessage)) {
|
||||
return;
|
||||
}
|
||||
String token = session.getId();
|
||||
try {
|
||||
// 解析请求
|
||||
Tuple tuple = WebSockets.parsePayload(((TextMessage) message).getPayload());
|
||||
if (tuple == null) {
|
||||
WebSockets.sendText(session, WsProtocol.ERROR.get());
|
||||
return;
|
||||
}
|
||||
TerminalClientOperate operate = tuple.get(0);
|
||||
String body = tuple.get(1);
|
||||
|
||||
// 建立连接
|
||||
if (operate == TerminalClientOperate.CONNECT) {
|
||||
// 建立连接
|
||||
if (session.getAttributes().get(WebSockets.AUTHED) != null) {
|
||||
return;
|
||||
}
|
||||
this.auth(session, body);
|
||||
return;
|
||||
}
|
||||
if (operate != TerminalClientOperate.KEY && operate != TerminalClientOperate.CLEAR) {
|
||||
return;
|
||||
}
|
||||
// 检查连接
|
||||
if (session.getAttributes().get(WebSockets.AUTHED) == null) {
|
||||
WebSockets.close(session, WsCloseCode.VALID);
|
||||
return;
|
||||
}
|
||||
// 检查是否只读
|
||||
final boolean readonly = Const.ENABLE.equals(session.getAttributes().get(WebSockets.READONLY));
|
||||
if (operate == TerminalClientOperate.KEY && readonly) {
|
||||
return;
|
||||
}
|
||||
// 获取连接
|
||||
String terminalToken = session.getAttributes().get(WebSockets.TOKEN).toString();
|
||||
IOperateHandler handler = terminalSessionManager.getSession(terminalToken);
|
||||
if (handler == null) {
|
||||
WebSockets.close(session, WsCloseCode.UNKNOWN_CONNECT);
|
||||
return;
|
||||
}
|
||||
// 操作
|
||||
handler.handleMessage(operate, body);
|
||||
} catch (Exception e) {
|
||||
log.error("terminal 处理操作异常 token: {}", token, e);
|
||||
WebSockets.close(session, WsCloseCode.RUNTIME_EXCEPTION);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||
log.error("terminal-watcher 操作异常拦截 token: {}", session.getId(), exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
log.info("terminal-watcher 关闭连接 token: {}, code: {}, reason: {}", session.getId(), status.getCode(), status.getReason());
|
||||
// 这时候主连接可能已经关了
|
||||
IOperateHandler handler = terminalSessionManager.getSession((String) session.getAttributes().get(WebSockets.TOKEN));
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
handler.getWatcher().removeWatcher(session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPartialMessages() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证
|
||||
*
|
||||
* @param session session
|
||||
* @param loginToken loginToken
|
||||
*/
|
||||
private void auth(WebSocketSession session, String loginToken) {
|
||||
// 检查参数
|
||||
Long userId = (Long) session.getAttributes().get(WebSockets.UID);
|
||||
// 获取登录用户
|
||||
UserDTO userDTO = passportService.getUserByToken(loginToken, null);
|
||||
if (userDTO == null || !userId.equals(userDTO.getId())) {
|
||||
WebSockets.close(session, WsCloseCode.IDENTITY_MISMATCH);
|
||||
return;
|
||||
}
|
||||
session.getAttributes().put(WebSockets.AUTHED, 1);
|
||||
// 获取连接
|
||||
String terminalToken = session.getAttributes().get(WebSockets.TOKEN).toString();
|
||||
IOperateHandler handler = terminalSessionManager.getSession(terminalToken);
|
||||
// 设置 watcher
|
||||
handler.getWatcher().addWatcher(session);
|
||||
WebSockets.sendText(session, WsProtocol.CONNECTED.get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
package cn.orionsec.ops.handler.terminal.watcher;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Threads;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.SchedulerPools;
|
||||
import cn.orionsec.ops.constant.ws.WsCloseCode;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TerminalWatcherProcessor implements ITerminalWatcherProcessor, Runnable {
|
||||
|
||||
private final Map<String, WebSocketSession> sessions;
|
||||
|
||||
private final LinkedBlockingQueue<byte[]> queue;
|
||||
|
||||
private volatile boolean run;
|
||||
|
||||
public TerminalWatcherProcessor() {
|
||||
this.sessions = Maps.newCurrentHashMap();
|
||||
this.queue = new LinkedBlockingQueue<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void watch() {
|
||||
this.run = true;
|
||||
Threads.start(this, SchedulerPools.TERMINAL_WATCHER_SCHEDULER);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void run() {
|
||||
while (run) {
|
||||
byte[] message = queue.poll(Const.MS_S_10, TimeUnit.MILLISECONDS);
|
||||
if (message == null || !run) {
|
||||
continue;
|
||||
}
|
||||
for (WebSocketSession session : sessions.values()) {
|
||||
WebSockets.sendText(session, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(byte[] message) {
|
||||
queue.add(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addWatcher(WebSocketSession session) {
|
||||
sessions.put(session.getId(), session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeWatcher(String id) {
|
||||
sessions.remove(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
sessions.forEach((k, s) -> WebSockets.close(s, WsCloseCode.EOF));
|
||||
sessions.clear();
|
||||
queue.clear();
|
||||
this.run = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
package cn.orionsec.ops.handler.webhook;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.collect.Lists;
|
||||
import com.dingtalk.api.DefaultDingTalkClient;
|
||||
import com.dingtalk.api.DingTalkClient;
|
||||
import com.dingtalk.api.request.OapiRobotSendRequest;
|
||||
import com.dingtalk.api.response.OapiRobotSendResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class DingRobotPusher implements IWebhookPusher {
|
||||
|
||||
/**
|
||||
* 推送 url
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 推送标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 推送内容
|
||||
*/
|
||||
private String text;
|
||||
|
||||
/**
|
||||
* @ 用户的手机号
|
||||
*/
|
||||
private List<String> atMobiles;
|
||||
|
||||
@Override
|
||||
public void push() {
|
||||
OapiRobotSendRequest.Markdown content = new OapiRobotSendRequest.Markdown();
|
||||
content.setTitle(title);
|
||||
content.setText(text);
|
||||
// 执行推送请求
|
||||
DingTalkClient client = new DefaultDingTalkClient(url);
|
||||
OapiRobotSendRequest request = new OapiRobotSendRequest();
|
||||
request.setMsgtype("markdown");
|
||||
request.setMarkdown(content);
|
||||
if (!Lists.isEmpty(atMobiles)) {
|
||||
OapiRobotSendRequest.At at = new OapiRobotSendRequest.At();
|
||||
at.setAtMobiles(atMobiles);
|
||||
request.setAt(at);
|
||||
}
|
||||
try {
|
||||
OapiRobotSendResponse response = client.execute(request);
|
||||
if (!response.isSuccess()) {
|
||||
log.error("钉钉机器人推送失败 url: {}", url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("钉钉机器人推送异常 url: {}", url, e);
|
||||
throw Exceptions.httpRequest(url, "ding push error", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
package cn.orionsec.ops.handler.webhook;
|
||||
|
||||
public interface IWebhookPusher {
|
||||
|
||||
/**
|
||||
* 执行推送
|
||||
*/
|
||||
void push();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.StandardContentType;
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
||||
import cn.orionsec.ops.annotation.IgnoreAuth;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.ResultCode;
|
||||
import cn.orionsec.ops.constant.common.EnableType;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.service.api.PassportService;
|
||||
import cn.orionsec.ops.utils.Currents;
|
||||
import cn.orionsec.ops.utils.UserHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class AuthenticateInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Resource
|
||||
private PassportService passportService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
// 是否跳过
|
||||
final boolean ignore = ((HandlerMethod) handler).hasMethodAnnotation(IgnoreAuth.class);
|
||||
HttpWrapper<?> rejectWrapper = null;
|
||||
String loginToken = Currents.getLoginToken(request);
|
||||
if (!Strings.isEmpty(loginToken)) {
|
||||
String ip = null;
|
||||
// 如果开启用户 ip 绑定 则获取 ip
|
||||
if (EnableType.of(SystemEnvAttr.LOGIN_IP_BIND.getValue()).getValue()) {
|
||||
ip = Servlets.getRemoteAddr(request);
|
||||
}
|
||||
// 获取用户登录信息
|
||||
UserDTO user = passportService.getUserByToken(loginToken, ip);
|
||||
if (user != null) {
|
||||
if (Const.DISABLE.equals(user.getUserStatus())) {
|
||||
rejectWrapper = HttpWrapper.of(ResultCode.USER_DISABLED);
|
||||
} else {
|
||||
UserHolder.set(user);
|
||||
}
|
||||
} else {
|
||||
rejectWrapper = HttpWrapper.of(ResultCode.UNAUTHORIZED);
|
||||
}
|
||||
} else if (!ignore) {
|
||||
rejectWrapper = HttpWrapper.of(ResultCode.UNAUTHORIZED);
|
||||
}
|
||||
// 匿名接口直接返回
|
||||
if (ignore) {
|
||||
return true;
|
||||
}
|
||||
// 驳回接口设置返回
|
||||
if (rejectWrapper != null) {
|
||||
response.setContentType(StandardContentType.APPLICATION_JSON);
|
||||
Servlets.transfer(response, rejectWrapper.toJsonString().getBytes());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
UserHolder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.ops.annotation.DemoDisableApi;
|
||||
import cn.orionsec.ops.constant.ResultCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
@Aspect
|
||||
@Slf4j
|
||||
@Order(20)
|
||||
public class DemoDisableApiAspect {
|
||||
|
||||
@Pointcut("@annotation(e)")
|
||||
public void disableApi(DemoDisableApi e) {
|
||||
}
|
||||
|
||||
@Before(value = "disableApi(e)", argNames = "e")
|
||||
public void beforeDisableApi(DemoDisableApi e) {
|
||||
throw Exceptions.httpWrapper(HttpWrapper.of(ResultCode.DEMO_DISABLE_API));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.StandardContentType;
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
||||
import cn.orionsec.ops.annotation.IgnoreCheck;
|
||||
import cn.orionsec.ops.constant.ResultCode;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class ExposeApiHeaderInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Value("${expose.api.access.header}")
|
||||
private String accessHeader;
|
||||
|
||||
@Value("${expose.api.access.secret}")
|
||||
private String accessSecret;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
// 是否跳过
|
||||
final boolean ignore = ((HandlerMethod) handler).hasMethodAnnotation(IgnoreCheck.class);
|
||||
if (ignore) {
|
||||
return true;
|
||||
}
|
||||
final boolean access = accessSecret.equals(request.getHeader(accessHeader));
|
||||
if (!access) {
|
||||
response.setContentType(StandardContentType.APPLICATION_JSON);
|
||||
Servlets.transfer(response, HttpWrapper.of(ResultCode.ILLEGAL_ACCESS).toJsonString().getBytes());
|
||||
}
|
||||
return access;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Booleans;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.ops.constant.KeyConst;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FileTransferNotifyInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
String token = WebSockets.getToken(request);
|
||||
String tokenKey = Strings.format(KeyConst.SFTP_SESSION_TOKEN, token);
|
||||
boolean access = Booleans.isTrue(redisTemplate.hasKey(tokenKey));
|
||||
log.info("sftp通知 尝试建立ws连接开始 token: {}, 结果: {}", token, access);
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.StandardContentType;
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.ResultCode;
|
||||
import cn.orionsec.ops.utils.Utils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class IpFilterInterceptor implements HandlerInterceptor {
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private boolean enable;
|
||||
|
||||
/**
|
||||
* 是否为白名单
|
||||
*/
|
||||
private boolean isWhiteList;
|
||||
|
||||
/**
|
||||
* 过滤器
|
||||
*/
|
||||
private List<String> filters;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
|
||||
// 停用
|
||||
if (!enable) {
|
||||
return true;
|
||||
}
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
String ip = Servlets.getRemoteAddr(request);
|
||||
// 本机不过滤
|
||||
if (Const.LOCALHOST_IP_V4.equals(ip)) {
|
||||
return true;
|
||||
}
|
||||
// 过滤
|
||||
boolean contains = false;
|
||||
for (String filter : filters) {
|
||||
if (Strings.isBlank(filter)) {
|
||||
continue;
|
||||
}
|
||||
// 检测
|
||||
contains = Utils.checkIpIn(ip, filter);
|
||||
if (contains) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 结果
|
||||
boolean pass;
|
||||
if (isWhiteList) {
|
||||
pass = contains;
|
||||
} else {
|
||||
pass = !contains;
|
||||
}
|
||||
// 返回
|
||||
if (!pass) {
|
||||
response.setContentType(StandardContentType.APPLICATION_JSON);
|
||||
Servlets.transfer(response, HttpWrapper.of(ResultCode.IP_BAN).toJsonString().getBytes());
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置启用类型
|
||||
*
|
||||
* @param enable 是否启用
|
||||
* @param isWhiteList 是否为白名单
|
||||
* @param filter 规则
|
||||
*/
|
||||
public void set(boolean enable, boolean isWhiteList, String filter) {
|
||||
this.enable = enable;
|
||||
this.isWhiteList = isWhiteList;
|
||||
if (Strings.isBlank(filter)) {
|
||||
this.enable = false;
|
||||
} else {
|
||||
this.filters = Arrays.stream(filter.split(Const.LF))
|
||||
.filter(Strings::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
if (filters.isEmpty()) {
|
||||
this.enable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.id.UUIds;
|
||||
import cn.orionsec.kit.lang.utils.Arrays1;
|
||||
import cn.orionsec.kit.lang.utils.Exceptions;
|
||||
import cn.orionsec.kit.lang.utils.time.Dates;
|
||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.utils.UserHolder;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.serializer.PropertyFilter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Order(10)
|
||||
@Component
|
||||
public class LogPrintInterceptor implements MethodInterceptor {
|
||||
|
||||
@Value("#{'${log.interceptor.ignore.fields:}'.split(',')}")
|
||||
private String[] ignoreFields;
|
||||
|
||||
/**
|
||||
* 请求序列
|
||||
*/
|
||||
public static final ThreadLocal<String> SEQ_HOLDER = ThreadLocal.withInitial(UUIds::random32);
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private static final ThreadLocal<Date> START_HOLDER = ThreadLocal.withInitial(Date::new);
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
// 打印开始日志
|
||||
this.beforeLogPrint(invocation);
|
||||
try {
|
||||
// 执行方法
|
||||
Object ret = invocation.proceed();
|
||||
// 返回打印
|
||||
this.afterReturnLogPrint(ret);
|
||||
return ret;
|
||||
} catch (Throwable t) {
|
||||
// 异常打印
|
||||
this.afterThrowingLogPrint(t);
|
||||
throw t;
|
||||
} finally {
|
||||
// 删除threadLocal
|
||||
SEQ_HOLDER.remove();
|
||||
START_HOLDER.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法进入打印
|
||||
*
|
||||
* @param invocation invocation
|
||||
*/
|
||||
public void beforeLogPrint(MethodInvocation invocation) {
|
||||
StringBuilder requestLog = new StringBuilder("\napi请求-开始-seq: ").append(SEQ_HOLDER.get()).append('\n');
|
||||
// 登录用户
|
||||
requestLog.append("\t当前用户: ").append(JSON.toJSONString(UserHolder.get())).append('\n');
|
||||
// http请求信息
|
||||
Optional.ofNullable(RequestContextHolder.getRequestAttributes())
|
||||
.map(s -> (ServletRequestAttributes) s)
|
||||
.map(ServletRequestAttributes::getRequest)
|
||||
.ifPresent(request -> {
|
||||
// url
|
||||
requestLog.append("\t").append(Servlets.getMethod(request)).append(" ")
|
||||
.append(Servlets.getRequestUrl(request)).append('\n');
|
||||
// query
|
||||
requestLog.append("\tip: ").append(Servlets.getRemoteAddr(request)).append('\n')
|
||||
.append("\tquery: ").append(Servlets.getQueryString(request)).append('\n');
|
||||
// header
|
||||
Servlets.getHeaderMap(request).forEach((hk, hv) -> requestLog.append('\t')
|
||||
.append(hk).append(": ")
|
||||
.append(hv).append('\n'));
|
||||
});
|
||||
// 方法信息
|
||||
Method method = invocation.getMethod();
|
||||
requestLog.append("\t开始时间: ").append(Dates.format(START_HOLDER.get(), Dates.YMD_HMSS)).append('\n')
|
||||
.append("\t方法签名: ").append(method.getDeclaringClass().getName()).append('#')
|
||||
.append(method.getName()).append("\n")
|
||||
.append("\t请求参数: ").append(this.argsToString(invocation.getArguments()));
|
||||
log.info(requestLog.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回打印
|
||||
*
|
||||
* @param ret return
|
||||
*/
|
||||
private void afterReturnLogPrint(Object ret) {
|
||||
Date endTime = new Date();
|
||||
// 响应日志
|
||||
StringBuilder responseLog = new StringBuilder("\napi请求-结束-seq: ").append(SEQ_HOLDER.get()).append('\n');
|
||||
responseLog.append("\t结束时间: ").append(Dates.format(endTime, Dates.YMD_HMSS))
|
||||
.append(" used: ").append(endTime.getTime() - START_HOLDER.get().getTime()).append("ms \n")
|
||||
.append("\t响应结果: ").append(this.argsToString(ret));
|
||||
log.info(responseLog.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 异常打印
|
||||
*
|
||||
* @param throwable ex
|
||||
*/
|
||||
private void afterThrowingLogPrint(Throwable throwable) {
|
||||
Date endTime = new Date();
|
||||
// 响应日志
|
||||
StringBuilder responseLog = new StringBuilder("\napi请求-异常-seq: ").append(SEQ_HOLDER.get()).append('\n');
|
||||
responseLog.append("\t结束时间: ").append(Dates.format(endTime, Dates.YMD_HMSS))
|
||||
.append(" used: ").append(endTime.getTime() - START_HOLDER.get().getTime()).append("ms \n")
|
||||
.append("\t异常摘要: ").append(Exceptions.getDigest(throwable));
|
||||
log.error(responseLog.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数转json
|
||||
*
|
||||
* @param o object
|
||||
* @return json
|
||||
*/
|
||||
private String argsToString(Object o) {
|
||||
try {
|
||||
if (ignoreFields.length == 1 && Const.EMPTY.equals(ignoreFields[0])) {
|
||||
// 不过滤
|
||||
return JSON.toJSONString(o);
|
||||
} else {
|
||||
return JSON.toJSONString(o, (PropertyFilter) (object, name, value) -> !Arrays1.contains(ignoreFields, name));
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return String.valueOf(o);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.constant.StandardContentType;
|
||||
import cn.orionsec.kit.lang.define.wrapper.HttpWrapper;
|
||||
import cn.orionsec.kit.lang.utils.Arrays1;
|
||||
import cn.orionsec.kit.web.servlet.web.Servlets;
|
||||
import cn.orionsec.ops.annotation.RequireRole;
|
||||
import cn.orionsec.ops.constant.ResultCode;
|
||||
import cn.orionsec.ops.constant.user.RoleType;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.utils.UserHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class RoleInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
RequireRole role = ((HandlerMethod) handler).getMethodAnnotation(RequireRole.class);
|
||||
if (role == null) {
|
||||
return true;
|
||||
}
|
||||
UserDTO user = UserHolder.get();
|
||||
if (user == null) {
|
||||
response.setContentType(StandardContentType.APPLICATION_JSON);
|
||||
Servlets.transfer(response, HttpWrapper.of(ResultCode.UNAUTHORIZED).toJsonString().getBytes());
|
||||
return false;
|
||||
}
|
||||
RoleType[] hasRoles = role.value();
|
||||
if (Arrays1.isEmpty(hasRoles)) {
|
||||
return true;
|
||||
}
|
||||
for (RoleType roleType : hasRoles) {
|
||||
if (roleType.getType().equals(user.getRoleType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
response.setContentType(StandardContentType.APPLICATION_JSON);
|
||||
Servlets.transfer(response, HttpWrapper.of(ResultCode.NO_PERMISSION).toJsonString().getBytes());
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.ops.constant.KeyConst;
|
||||
import cn.orionsec.ops.entity.dto.file.FileTailDTO;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TailFileInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
String token = WebSockets.getToken(request);
|
||||
String tokenKey = Strings.format(KeyConst.FILE_TAIL_ACCESS_TOKEN, token);
|
||||
String tokenValue = redisTemplate.opsForValue().get(tokenKey);
|
||||
boolean access = false;
|
||||
if (!Strings.isBlank(tokenValue)) {
|
||||
// 设置信息
|
||||
access = true;
|
||||
attributes.put(WebSockets.CONFIG, JSON.parseObject(tokenValue, FileTailDTO.class));
|
||||
attributes.put(WebSockets.TOKEN, token);
|
||||
// 删除 token
|
||||
redisTemplate.delete(tokenKey);
|
||||
}
|
||||
log.info("tail 尝试建立ws连接开始 token: {}, 结果: {}", token, access);
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.ops.constant.KeyConst;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TerminalAccessInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
// 获取 token
|
||||
String token = WebSockets.getToken(request);
|
||||
String tokenKey = Strings.format(KeyConst.TERMINAL_ACCESS_TOKEN, token);
|
||||
String tokenValue = redisTemplate.opsForValue().get(tokenKey);
|
||||
boolean access = false;
|
||||
if (!Strings.isBlank(tokenValue)) {
|
||||
// 设置用户机器信息
|
||||
access = true;
|
||||
String[] pair = tokenValue.split("_");
|
||||
attributes.put(WebSockets.UID, Long.valueOf(pair[0]));
|
||||
attributes.put(WebSockets.MID, Long.valueOf(pair[1]));
|
||||
// 删除 token
|
||||
redisTemplate.delete(tokenKey);
|
||||
}
|
||||
log.info("terminal尝试打开ws连接开始 token: {}, 结果: {}", token, access);
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.ops.constant.KeyConst;
|
||||
import cn.orionsec.ops.entity.dto.terminal.TerminalWatcherDTO;
|
||||
import cn.orionsec.ops.utils.WebSockets;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TerminalWatcherInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
// 获取 token
|
||||
String token = WebSockets.getToken(request);
|
||||
String tokenKey = Strings.format(KeyConst.TERMINAL_WATCHER_TOKEN, token);
|
||||
String tokenValue = redisTemplate.opsForValue().get(tokenKey);
|
||||
boolean access = false;
|
||||
if (!Strings.isBlank(tokenValue)) {
|
||||
access = true;
|
||||
TerminalWatcherDTO watcher = JSON.parseObject(tokenValue, TerminalWatcherDTO.class);
|
||||
attributes.put(WebSockets.UID, watcher.getUserId());
|
||||
attributes.put(WebSockets.TOKEN, watcher.getToken());
|
||||
attributes.put(WebSockets.READONLY, watcher.getReadonly());
|
||||
// 删除 token
|
||||
redisTemplate.delete(tokenKey);
|
||||
}
|
||||
log.info("terminal尝试监视ws连接开始 token: {}, 结果: {}", token, access);
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.kit.lang.utils.Strings;
|
||||
import cn.orionsec.kit.lang.utils.collect.Maps;
|
||||
import cn.orionsec.ops.constant.Const;
|
||||
import cn.orionsec.ops.constant.KeyConst;
|
||||
import cn.orionsec.ops.constant.common.EnableType;
|
||||
import cn.orionsec.ops.constant.event.EventKeys;
|
||||
import cn.orionsec.ops.constant.event.EventType;
|
||||
import cn.orionsec.ops.constant.system.SystemEnvAttr;
|
||||
import cn.orionsec.ops.dao.UserInfoDAO;
|
||||
import cn.orionsec.ops.entity.dto.user.UserDTO;
|
||||
import cn.orionsec.ops.service.api.UserEventLogService;
|
||||
import cn.orionsec.ops.utils.Currents;
|
||||
import cn.orionsec.ops.utils.EventParamsHolder;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class UserActiveInterceptor implements HandlerInterceptor {
|
||||
|
||||
/**
|
||||
* key: 用户id
|
||||
* value: 活跃时间戳
|
||||
*/
|
||||
private final Map<Long, Long> activeUsers = Maps.newCurrentHashMap();
|
||||
|
||||
@Resource
|
||||
private UserInfoDAO userInfoDAO;
|
||||
|
||||
@Resource
|
||||
private UserEventLogService userEventLogService;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 处理
|
||||
this.handleActive();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户活跃信息
|
||||
*/
|
||||
private void handleActive() {
|
||||
UserDTO user = Currents.getUser();
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
Long before = activeUsers.put(user.getId(), now);
|
||||
if (before == null) {
|
||||
// 应用启动/多端登录(单端登出) token有效
|
||||
this.refreshActive(user);
|
||||
return;
|
||||
}
|
||||
// 获取活跃域值
|
||||
long activeThresholdHour = Long.parseLong(SystemEnvAttr.LOGIN_TOKEN_AUTO_RENEW_THRESHOLD.getValue());
|
||||
long activeThreshold = TimeUnit.HOURS.toMillis(activeThresholdHour);
|
||||
if (before + activeThreshold < now) {
|
||||
// 超过活跃域值
|
||||
this.refreshActive(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新活跃
|
||||
*
|
||||
* @param user user
|
||||
*/
|
||||
private void refreshActive(UserDTO user) {
|
||||
Long userId = user.getId();
|
||||
// 刷新登录时间
|
||||
userInfoDAO.updateLastLoginTime(userId);
|
||||
// 记录日志
|
||||
EventParamsHolder.addParam(EventKeys.REFRESH_LOGIN, Const.ENABLE);
|
||||
EventParamsHolder.setDefaultEventParams();
|
||||
userEventLogService.recordLog(EventType.LOGIN, true);
|
||||
// 如果开启自动续签 刷新登录token 绑定token
|
||||
if (EnableType.of(SystemEnvAttr.LOGIN_TOKEN_AUTO_RENEW.getValue()).getValue()) {
|
||||
long expire = Long.parseLong(SystemEnvAttr.LOGIN_TOKEN_EXPIRE.getValue());
|
||||
String loginKey = Strings.format(KeyConst.LOGIN_TOKEN_KEY, userId);
|
||||
String bindKey = Strings.format(KeyConst.LOGIN_TOKEN_BIND_KEY, userId, user.getCurrentBindTimestamp());
|
||||
redisTemplate.expire(loginKey, expire, TimeUnit.HOURS);
|
||||
redisTemplate.expire(bindKey, expire, TimeUnit.HOURS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置活跃时间
|
||||
*
|
||||
* @param id id
|
||||
* @param time 登录时间
|
||||
*/
|
||||
public void setActiveTime(Long id, Long time) {
|
||||
activeUsers.put(id, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除活跃时间
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
public void deleteActiveTime(Long id) {
|
||||
activeUsers.remove(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
package cn.orionsec.ops.interceptor;
|
||||
|
||||
import cn.orionsec.ops.annotation.EventLog;
|
||||
import cn.orionsec.ops.service.api.UserEventLogService;
|
||||
import cn.orionsec.ops.utils.EventParamsHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
@Aspect
|
||||
@Slf4j
|
||||
@Order(30)
|
||||
public class UserEventLogAspect {
|
||||
|
||||
@Resource
|
||||
private UserEventLogService userEventLogService;
|
||||
|
||||
@Pointcut("@annotation(e)")
|
||||
public void eventLogPoint(EventLog e) {
|
||||
}
|
||||
|
||||
@Before(value = "eventLogPoint(e)", argNames = "e")
|
||||
public void beforeLogRecord(EventLog e) {
|
||||
EventParamsHolder.remove();
|
||||
// 设置默认参数
|
||||
EventParamsHolder.setDefaultEventParams();
|
||||
}
|
||||
|
||||
@AfterReturning(pointcut = "eventLogPoint(e)", argNames = "e")
|
||||
public void afterLogRecord(EventLog e) {
|
||||
userEventLogService.recordLog(e.value(), true);
|
||||
}
|
||||
|
||||
@AfterThrowing(pointcut = "eventLogPoint(e)", argNames = "e")
|
||||
public void afterLogRecordThrowing(EventLog e) {
|
||||
userEventLogService.recordLog(e.value(), false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
package cn.orionsec.ops.service.api;
|
||||
|
||||
import cn.orionsec.ops.entity.domain.AlarmGroupNotifyDO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AlarmGroupNotifyService {
|
||||
|
||||
/**
|
||||
* 通过 groupId 查询
|
||||
*
|
||||
* @param groupId groupId
|
||||
* @return rows
|
||||
*/
|
||||
List<AlarmGroupNotifyDO> selectByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 通过 groupId 查询
|
||||
*
|
||||
* @param groupIdList groupIdList
|
||||
* @return rows
|
||||
*/
|
||||
List<AlarmGroupNotifyDO> selectByGroupIdList(List<Long> groupIdList);
|
||||
|
||||
/**
|
||||
* 通过 groupId 删除
|
||||
*
|
||||
* @param groupId groupId
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 通过 webhookId 删除
|
||||
*
|
||||
* @param webhookId webhookId
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteByWebhookId(Long webhookId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
package cn.orionsec.ops.service.api;
|
||||
|
||||
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
|
||||
import cn.orionsec.ops.entity.request.alarm.AlarmGroupRequest;
|
||||
import cn.orionsec.ops.entity.vo.alarm.AlarmGroupVO;
|
||||
|
||||
public interface AlarmGroupService {
|
||||
|
||||
/**
|
||||
* 添加报警组
|
||||
*
|
||||
* @param request request
|
||||
* @return id
|
||||
*/
|
||||
Long addAlarmGroup(AlarmGroupRequest request);
|
||||
|
||||
/**
|
||||
* 更新报警组
|
||||
*
|
||||
* @param request request
|
||||
* @return effect
|
||||
*/
|
||||
Integer updateAlarmGroup(AlarmGroupRequest request);
|
||||
|
||||
/**
|
||||
* 删除报警组
|
||||
*
|
||||
* @param id id
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteAlarmGroup(Long id);
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*
|
||||
* @param request request
|
||||
* @return rows
|
||||
*/
|
||||
DataGrid<AlarmGroupVO> getAlarmGroupList(AlarmGroupRequest request);
|
||||
|
||||
/**
|
||||
* 查询详情
|
||||
*
|
||||
* @param id id
|
||||
* @return row
|
||||
*/
|
||||
AlarmGroupVO getAlarmGroupDetail(Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
package cn.orionsec.ops.service.api;
|
||||
|
||||
import cn.orionsec.ops.entity.domain.AlarmGroupUserDO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AlarmGroupUserService {
|
||||
|
||||
/**
|
||||
* 通过 groupId 查询
|
||||
*
|
||||
* @param groupId groupId
|
||||
* @return rows
|
||||
*/
|
||||
List<AlarmGroupUserDO> selectByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 通过 groupId 查询
|
||||
*
|
||||
* @param groupIdList groupIdList
|
||||
* @return rows
|
||||
*/
|
||||
List<AlarmGroupUserDO> selectByGroupIdList(List<Long> groupIdList);
|
||||
|
||||
/**
|
||||
* 通过 groupId 删除
|
||||
*
|
||||
* @param groupId groupId
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteByGroupId(Long groupId);
|
||||
|
||||
/**
|
||||
* 通过 userId 删除
|
||||
*
|
||||
* @param userId userId
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteByUserId(Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
package cn.orionsec.ops.service.api;
|
||||
|
||||
import cn.orionsec.ops.constant.app.StageType;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationActionLogDO;
|
||||
import cn.orionsec.ops.entity.vo.app.ApplicationActionLogVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ApplicationActionLogService {
|
||||
|
||||
/**
|
||||
* 通过 id 查询详情
|
||||
*
|
||||
* @param id id
|
||||
* @return detail
|
||||
*/
|
||||
ApplicationActionLogVO getDetailById(Long id);
|
||||
|
||||
/**
|
||||
* 通过 id 查询状态
|
||||
*
|
||||
* @param id id
|
||||
* @return detail
|
||||
*/
|
||||
ApplicationActionLogVO getStatusById(Long id);
|
||||
|
||||
/**
|
||||
* 获取操作执行日志
|
||||
*
|
||||
* @param relId relId
|
||||
* @param stageType stageType
|
||||
* @return rows
|
||||
*/
|
||||
List<ApplicationActionLogVO> getActionLogsByRelId(Long relId, StageType stageType);
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param relId relId
|
||||
* @param stageType stageType
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteByRelId(Long relId, StageType stageType);
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param relIdList relIdList
|
||||
* @param stageType stageType
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteByRelIdList(List<Long> relIdList, StageType stageType);
|
||||
|
||||
/**
|
||||
* 通过 relId 查询 action
|
||||
*
|
||||
* @param relId relId
|
||||
* @param stageType stageType
|
||||
* @return action
|
||||
*/
|
||||
List<ApplicationActionLogDO> selectActionByRelId(Long relId, StageType stageType);
|
||||
|
||||
/**
|
||||
* 通过 relIdList 查询 action
|
||||
*
|
||||
* @param relIdList relIdList
|
||||
* @param stageType stageType
|
||||
* @return action
|
||||
*/
|
||||
List<ApplicationActionLogDO> selectActionByRelIdList(List<Long> relIdList, StageType stageType);
|
||||
|
||||
/**
|
||||
* 更新 action
|
||||
*
|
||||
* @param record record
|
||||
*/
|
||||
void updateActionById(ApplicationActionLogDO record);
|
||||
|
||||
/**
|
||||
* 获取操作执行日志路径
|
||||
*
|
||||
* @param id id
|
||||
* @return path
|
||||
*/
|
||||
String getActionLogPath(Long id);
|
||||
|
||||
/**
|
||||
* 设置操作状态
|
||||
*
|
||||
* @param relId relId
|
||||
* @param stageType stageType
|
||||
*/
|
||||
void resetActionStatus(Long relId, StageType stageType);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
package cn.orionsec.ops.service.api;
|
||||
|
||||
import cn.orionsec.ops.entity.domain.ApplicationActionDO;
|
||||
import cn.orionsec.ops.entity.request.app.ApplicationConfigRequest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public interface ApplicationActionService {
|
||||
|
||||
/**
|
||||
* 删除发布流程
|
||||
*
|
||||
* @param appId appId
|
||||
* @param profileId profileId
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteAppActionByAppProfileId(Long appId, Long profileId);
|
||||
|
||||
/**
|
||||
* 获取发布流程
|
||||
*
|
||||
* @param appId appId
|
||||
* @param profileId profileId
|
||||
* @param stageType stageType
|
||||
* @return actions
|
||||
*/
|
||||
List<ApplicationActionDO> getAppProfileActions(Long appId, Long profileId, Integer stageType);
|
||||
|
||||
/**
|
||||
* 通过appId profileId查询操作流程数量数量
|
||||
*
|
||||
* @param appId appId
|
||||
* @param profileId profileId
|
||||
* @param stageType stageType
|
||||
* @return count
|
||||
*/
|
||||
Integer getAppProfileActionCount(Long appId, Long profileId, Integer stageType);
|
||||
|
||||
/**
|
||||
* 配置app发布流程
|
||||
*
|
||||
* @param request request
|
||||
*/
|
||||
void configAppAction(ApplicationConfigRequest request);
|
||||
|
||||
/**
|
||||
* 同步app操作流程
|
||||
*
|
||||
* @param appId appId
|
||||
* @param profileId profileId
|
||||
* @param syncProfileId 需要同步的profileId
|
||||
*/
|
||||
void syncAppProfileAction(Long appId, Long profileId, Long syncProfileId);
|
||||
|
||||
/**
|
||||
* 复制发布流程
|
||||
*
|
||||
* @param appId appId
|
||||
* @param targetAppId targetAppId
|
||||
*/
|
||||
void copyAppAction(Long appId, Long targetAppId);
|
||||
|
||||
/**
|
||||
* 获取app是否已配置
|
||||
*
|
||||
* @param profileId profileId
|
||||
* @param appIdList appIdList
|
||||
* @return appId, isConfig
|
||||
*/
|
||||
Map<Long, Boolean> getAppIsConfig(Long profileId, List<Long> appIdList);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
|
||||
package cn.orionsec.ops.service.api;
|
||||
|
||||
import cn.orionsec.kit.lang.define.wrapper.DataGrid;
|
||||
import cn.orionsec.ops.entity.domain.ApplicationBuildDO;
|
||||
import cn.orionsec.ops.entity.request.app.ApplicationBuildRequest;
|
||||
import cn.orionsec.ops.entity.vo.app.ApplicationBuildReleaseListVO;
|
||||
import cn.orionsec.ops.entity.vo.app.ApplicationBuildStatusVO;
|
||||
import cn.orionsec.ops.entity.vo.app.ApplicationBuildVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ApplicationBuildService {
|
||||
|
||||
/**
|
||||
* 提交执行
|
||||
*
|
||||
* @param request request
|
||||
* @param execute 是否提交任务
|
||||
* @return id
|
||||
*/
|
||||
Long submitBuildTask(ApplicationBuildRequest request, boolean execute);
|
||||
|
||||
/**
|
||||
* 获取构建列表
|
||||
*
|
||||
* @param request request
|
||||
* @return rows
|
||||
*/
|
||||
DataGrid<ApplicationBuildVO> getBuildList(ApplicationBuildRequest request);
|
||||
|
||||
/**
|
||||
* 获取构建详情
|
||||
*
|
||||
* @param id id
|
||||
* @return row
|
||||
*/
|
||||
ApplicationBuildVO getBuildDetail(Long id);
|
||||
|
||||
/**
|
||||
* 查询构建状态
|
||||
*
|
||||
* @param id id
|
||||
* @return status
|
||||
*/
|
||||
ApplicationBuildStatusVO getBuildStatus(Long id);
|
||||
|
||||
/**
|
||||
* 查询构建状态列表
|
||||
*
|
||||
* @param buildIdList id
|
||||
* @return key: id value: status
|
||||
*/
|
||||
List<ApplicationBuildStatusVO> getBuildStatusList(List<Long> buildIdList);
|
||||
|
||||
/**
|
||||
* 停止构建
|
||||
*
|
||||
* @param id id
|
||||
*/
|
||||
void terminateBuildTask(Long id);
|
||||
|
||||
/**
|
||||
* 输入命令
|
||||
*
|
||||
* @param id id
|
||||
* @param command command
|
||||
*/
|
||||
void writeBuildTask(Long id, String command);
|
||||
|
||||
/**
|
||||
* 删除构建
|
||||
*
|
||||
* @param idList idList
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteBuildTask(List<Long> idList);
|
||||
|
||||
/**
|
||||
* 重新构建
|
||||
*
|
||||
* @param id id
|
||||
* @return id
|
||||
*/
|
||||
Long rebuild(Long id);
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id id
|
||||
* @return row
|
||||
*/
|
||||
ApplicationBuildDO selectById(Long id);
|
||||
|
||||
/**
|
||||
* 获取构建日志路径
|
||||
*
|
||||
* @param id id
|
||||
* @return path
|
||||
*/
|
||||
String getBuildLogPath(Long id);
|
||||
|
||||
/**
|
||||
* 获取构建产物路径
|
||||
*
|
||||
* @param id id
|
||||
* @return path
|
||||
*/
|
||||
String getBuildBundlePath(Long id);
|
||||
|
||||
/**
|
||||
* 检查并且获取构建目录
|
||||
*
|
||||
* @param build build
|
||||
* @return 构建产物路径
|
||||
*/
|
||||
String checkBuildBundlePath(ApplicationBuildDO build);
|
||||
|
||||
/**
|
||||
* 获取构建发布序列
|
||||
*
|
||||
* @param appId appId
|
||||
* @param profileId profileId
|
||||
* @return rows
|
||||
*/
|
||||
List<ApplicationBuildReleaseListVO> getBuildReleaseList(Long appId, Long profileId);
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user