🔨 执行命令.

This commit is contained in:
lijiahang
2024-03-14 19:46:05 +08:00
parent e98bace51b
commit 6bd2640af7
25 changed files with 900 additions and 181 deletions

View File

@@ -7,6 +7,7 @@ import com.orion.ops.framework.web.core.annotation.RestWrapper;
import com.orion.ops.module.asset.define.operator.ExecOperatorType;
import com.orion.ops.module.asset.entity.request.exec.ExecCommandRequest;
import com.orion.ops.module.asset.entity.request.exec.ExecInterruptRequest;
import com.orion.ops.module.asset.entity.request.exec.ReExecCommandRequest;
import com.orion.ops.module.asset.entity.vo.ExecCommandVO;
import com.orion.ops.module.asset.service.ExecService;
import io.swagger.v3.oas.annotations.Operation;
@@ -41,10 +42,18 @@ public class ExecController {
@PostMapping("/exec-command")
@Operation(summary = "批量执行命令")
@PreAuthorize("@ss.hasPermission('asset:exec:exec-command')")
public ExecCommandVO execCommand(@RequestBody ExecCommandRequest request) {
public ExecCommandVO execCommand(@Validated @RequestBody ExecCommandRequest request) {
return execService.execCommand(request);
}
@OperatorLog(ExecOperatorType.EXEC_COMMAND)
@PostMapping("/re-exec-command")
@Operation(summary = "重新执行命令")
@PreAuthorize("@ss.hasPermission('asset:exec:exec-command')")
public ExecCommandVO reExecCommand(@Validated @RequestBody ReExecCommandRequest request) {
return execService.reExecCommand(request.getLogId());
}
@OperatorLog(ExecOperatorType.INTERRUPT_EXEC)
@PutMapping("/interrupt")
@Operation(summary = "中断执行命令")
@@ -66,7 +75,6 @@ public class ExecController {
}
// TODO tail log
// TODO 重新执行
// TODO 删除时 中断
// TODO parameterSchema 存储
}

View File

@@ -4,6 +4,8 @@ import com.orion.ops.framework.mybatis.core.mapper.IMapper;
import com.orion.ops.module.asset.entity.domain.ExecHostLogDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 批量执行主机日志 Mapper 接口
*
@@ -14,4 +16,18 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ExecHostLogDAO extends IMapper<ExecHostLogDO> {
/**
* 通过 logId 查询
*
* @param logId logId
* @return rows
*/
default List<ExecHostLogDO> selectByLogId(Long logId) {
return this.of()
.createWrapper()
.eq(ExecHostLogDO::getLogId, logId)
.then()
.list();
}
}

View File

@@ -22,9 +22,6 @@ import java.util.List;
@Schema(name = "ExecCommandRequest", description = "批量执行命令 请求对象")
public class ExecCommandRequest {
@Schema(description = "执行模板id")
private Long templateId;
@Size(max = 128)
@Schema(description = "执行描述")
private String description;

View File

@@ -0,0 +1,24 @@
package com.orion.ops.module.asset.entity.request.exec;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
/**
* 重新执行命令 请求对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/11 11:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ReExecCommandRequest", description = "重新执行命令 请求对象")
public class ReExecCommandRequest {
@NonNull
@Schema(description = "logId")
private Long logId;
}

View File

@@ -20,6 +20,14 @@ public interface ExecService {
*/
ExecCommandVO execCommand(ExecCommandRequest request);
/**
* 重新执行命令
*
* @param id id
* @return result
*/
ExecCommandVO reExecCommand(Long id);
/**
* 中断命令执行
*

View File

@@ -9,6 +9,9 @@ import com.orion.ops.module.asset.convert.ExecHostLogConvert;
import com.orion.ops.module.asset.dao.ExecHostLogDAO;
import com.orion.ops.module.asset.entity.domain.ExecHostLogDO;
import com.orion.ops.module.asset.entity.vo.ExecHostLogVO;
import com.orion.ops.module.asset.handler.host.exec.handler.IExecCommandHandler;
import com.orion.ops.module.asset.handler.host.exec.handler.IExecTaskHandler;
import com.orion.ops.module.asset.handler.host.exec.manager.ExecManager;
import com.orion.ops.module.asset.service.ExecHostLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -16,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
/**
* 批量执行主机日志 服务实现类
@@ -31,6 +35,9 @@ public class ExecHostLogServiceImpl implements ExecHostLogService {
@Resource
private ExecHostLogDAO execHostLogDAO;
@Resource
private ExecManager execManager;
@Override
public List<ExecHostLogVO> getExecHostLogList(Long logId) {
return execHostLogDAO.of()
@@ -65,6 +72,14 @@ public class ExecHostLogServiceImpl implements ExecHostLogService {
// 检查数据是否存在
ExecHostLogDO record = execHostLogDAO.selectById(id);
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
// 中断
Optional.ofNullable(record.getLogId())
.map(execManager::getTask)
.map(IExecTaskHandler::getHandlers)
.flatMap(s -> s.stream()
.filter(h -> h.getHostId().equals(record.getHostId()))
.findFirst())
.ifPresent(IExecCommandHandler::interrupted);
// 删除
int effect = execHostLogDAO.deleteById(id);
log.info("ExecHostLogService-deleteExecHostLogById id: {}, effect: {}", id, effect);

View File

@@ -18,6 +18,8 @@ import com.orion.ops.module.asset.entity.request.exec.ExecLogQueryRequest;
import com.orion.ops.module.asset.entity.vo.ExecHostLogVO;
import com.orion.ops.module.asset.entity.vo.ExecLogStatusVO;
import com.orion.ops.module.asset.entity.vo.ExecLogVO;
import com.orion.ops.module.asset.handler.host.exec.handler.IExecTaskHandler;
import com.orion.ops.module.asset.handler.host.exec.manager.ExecManager;
import com.orion.ops.module.asset.service.ExecHostLogService;
import com.orion.ops.module.asset.service.ExecLogService;
import lombok.extern.slf4j.Slf4j;
@@ -26,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -48,6 +51,9 @@ public class ExecLogServiceImpl implements ExecLogService {
@Resource
private ExecHostLogService execHostLogService;
@Resource
private ExecManager execManager;
@Override
public DataGrid<ExecLogVO> getExecLogPage(ExecLogQueryRequest request) {
// 条件
@@ -74,6 +80,7 @@ public class ExecLogServiceImpl implements ExecLogService {
List<ExecHostLogVO> hostList = execHostLogDAO.of()
.createWrapper()
.select(ExecHostLogDO::getId,
ExecHostLogDO::getLogId,
ExecHostLogDO::getStatus,
ExecHostLogDO::getStartTime,
ExecHostLogDO::getFinishTime,
@@ -101,6 +108,8 @@ public class ExecLogServiceImpl implements ExecLogService {
// 检查数据是否存在
ExecLogDO record = execLogDAO.selectById(id);
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
// 中断命令执行
this.interruptedTask(Lists.singleton(id));
// 删除执行日志
int effect = execLogDAO.deleteById(id);
// 删除主机日志
@@ -115,6 +124,8 @@ public class ExecLogServiceImpl implements ExecLogService {
@Transactional(rollbackFor = Exception.class)
public Integer deleteExecLogByIdList(List<Long> idList) {
log.info("ExecLogService-deleteExecLogByIdList idList: {}", idList);
// 中断命令执行
this.interruptedTask(idList);
// 删除执行日志
int effect = execLogDAO.deleteBatchIds(idList);
// 删除主机日志
@@ -138,6 +149,8 @@ public class ExecLogServiceImpl implements ExecLogService {
.collect(Collectors.toList());
int effect = 0;
if (!idList.isEmpty()) {
// 中断命令执行
this.interruptedTask(idList);
// 删除执行日志
effect = execLogDAO.delete(wrapper);
// 删除主机日志
@@ -170,4 +183,16 @@ public class ExecLogServiceImpl implements ExecLogService {
.orderByDesc(ExecLogDO::getId);
}
/**
* 中断任务
*
* @param idList idList
*/
private void interruptedTask(List<Long> idList) {
idList.stream()
.map(execManager::getTask)
.filter(Objects::nonNull)
.forEach(IExecTaskHandler::interrupted);
}
}

View File

@@ -61,7 +61,7 @@ import java.util.stream.Collectors;
public class ExecServiceImpl implements ExecService {
private static final ReplacementFormatter FORMATTER = ReplacementFormatters.create("@{{ ", " }}")
.noMatchStrategy(NoMatchStrategy.EMPTY);
.noMatchStrategy(NoMatchStrategy.KEEP);
@Resource
private FileClient logsFileClient;
@@ -124,23 +124,10 @@ public class ExecServiceImpl implements ExecService {
.build();
}).collect(Collectors.toList());
execHostLogDAO.insertBatch(execHostLogs);
// 开始执行
ExecCommandDTO exec = ExecCommandDTO.builder()
.logId(execId)
.timeout(request.getTimeout())
.hosts(execHostLogs.stream()
.map(s -> ExecCommandHostDTO.builder()
.hostId(s.getHostId())
.hostLogId(s.getId())
.command(s.getCommand())
.timeout(request.getTimeout())
.logPath(s.getLogPath())
.build())
.collect(Collectors.toList()))
.build();
ExecTaskExecutors.start(exec);
// 操作日志
OperatorLogs.add(OperatorLogs.ID, execId);
// 开始执行
this.startExec(execLog, execHostLogs);
// 返回
List<ExecCommandHostVO> hostResult = execHostLogs.stream()
.map(s -> ExecCommandHostVO.builder()
@@ -154,6 +141,29 @@ public class ExecServiceImpl implements ExecService {
.build();
}
@Override
public ExecCommandVO reExecCommand(Long logId) {
log.info("ExecService.reExecCommand start logId: {}", logId);
// 获取执行记录
ExecLogDO execLog = execLogDAO.selectById(logId);
Valid.notNull(execLog, ErrorMessage.DATA_ABSENT);
// 获取执行主机
List<ExecHostLogDO> hostLogs = execHostLogDAO.selectByLogId(logId);
Valid.notEmpty(hostLogs, ErrorMessage.DATA_ABSENT);
List<Long> hostIdList = hostLogs.stream()
.map(ExecHostLogDO::getHostId)
.collect(Collectors.toList());
// 调用执行方法
ExecCommandRequest request = ExecCommandRequest.builder()
.description(execLog.getDescription())
.timeout(execLog.getTimeout())
.command(execLog.getCommand())
.parameter(hostLogs.get(0).getParameter())
.hostIdList(hostIdList)
.build();
return this.execCommand(request);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void interruptExec(Long logId) {
@@ -250,6 +260,29 @@ public class ExecServiceImpl implements ExecService {
}
}
/**
* 开始执行命令
*
* @param execLog execLog
* @param execHostLogs hostLogs
*/
private void startExec(ExecLogDO execLog, List<ExecHostLogDO> execHostLogs) {
ExecCommandDTO exec = ExecCommandDTO.builder()
.logId(execLog.getId())
.timeout(execLog.getTimeout())
.hosts(execHostLogs.stream()
.map(s -> ExecCommandHostDTO.builder()
.hostId(s.getHostId())
.hostLogId(s.getId())
.command(s.getCommand())
.timeout(execLog.getTimeout())
.logPath(s.getLogPath())
.build())
.collect(Collectors.toList()))
.build();
ExecTaskExecutors.start(exec);
}
/**
* 构建日志路径
*

View File

@@ -4,11 +4,12 @@ import axios from 'axios';
* 执行命令请求
*/
export interface ExecCommandRequest {
templateId?: number;
logId?: number;
description?: string;
timeout?: number;
command?: string;
parameter?: string;
parameterSchema?: string;
hostIdList?: number[];
}
@@ -38,6 +39,13 @@ export function execCommand(request: ExecCommandRequest) {
return axios.post<ExecCommandResponse>('/asset/exec/exec-command', request);
}
/**
* 重新执行命令
*/
export function reExecCommand(request: ExecCommandRequest) {
return axios.post<ExecCommandResponse>('/asset/exec/re-exec-command', request);
}
/**
* 中断执行命令
*/

View File

@@ -121,7 +121,7 @@
});
// 打开
const open = async (hostIdList: Array<number>) => {
const open = async (hostIdList: Array<number> = []) => {
setVisible(true);
// 加载主机列表
await fetchHosts();

View File

@@ -0,0 +1,56 @@
// 模板参数
export interface TemplateParam {
name?: string;
default?: string;
desc?: string;
}
// 内置参数
export const builtinsParams: Array<TemplateParam> = [
{
name: 'hostId',
desc: '执行主机id'
}, {
name: 'hostName',
desc: '执行主机名称'
}, {
name: 'hostCode',
desc: '执行主机编码'
}, {
name: 'hostAddress',
desc: '执行主机地址'
}, {
name: 'userId',
desc: '执行用户id'
}, {
name: 'username',
desc: '执行用户名'
}, {
name: 'execId',
desc: '执行记录id'
}, {
name: 'uuid',
desc: '生成任务维度 uuid'
}, {
name: 'uuidShort',
desc: '生成任务维度 uuid 无 \'-\''
}, {
name: 'hostUuid',
desc: '生成机器维度 uuid'
}, {
name: 'hostUuidShort',
desc: '生成机器维度 uuid 无 \'-\''
}, {
name: 'timestampMillis',
desc: '时间戳毫秒'
}, {
name: 'timestamp',
desc: '时间戳'
}, {
name: 'date',
desc: '执行时间 yyyy-MM-dd'
}, {
name: 'datetime',
desc: '执行时间 yyyy-MM-dd HH:mm:ss'
},
];

View File

@@ -0,0 +1,86 @@
<template>
<editor language="shell"
:suggestions="false" />
</template>
<script lang="ts">
export default {
name: 'execEditor'
};
</script>
<script lang="ts" setup>
import type { TemplateParam } from './const';
import { onMounted, onUnmounted, ref } from 'vue';
import { builtinsParams } from './const';
import * as monaco from 'monaco-editor';
import { language } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
const props = defineProps<{
parameter?: Array<TemplateParam>
}>();
const suggestionsDispose = ref();
// 加载代码提示
onMounted(() => {
if (suggestionsDispose.value) {
return;
}
// 代码提示
suggestionsDispose.value = monaco.languages.registerCompletionItemProvider('shell', {
provideCompletionItems() {
const suggestions: any = [];
language.keywords?.forEach((item: any) => {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item
});
});
language.builtins?.forEach((item: any) => {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
});
});
// 内置参数提示
builtinsParams.forEach(s => {
suggestions.push({
label: s.name,
kind: monaco.languages.CompletionItemKind.Function,
insertText: `@{{ ${s.name} }}`,
detail: s.desc || '',
});
});
// 命令参数提示
props.parameter?.forEach(s => {
if (!s.name) {
return;
}
suggestions.push({
label: s.name,
kind: monaco.languages.CompletionItemKind.Function,
insertText: `@{{ ${s.name} }}`,
detail: s.desc || '',
});
});
return {
suggestions: [...new Set(suggestions)],
};
},
});
});
// 卸载代码提示
onUnmounted(() => {
suggestionsDispose.value?.dispose();
suggestionsDispose.value = undefined;
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -7,9 +7,9 @@ const EXEC: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT,
children: [
{
name: 'execTemplate',
path: '/exec-template',
component: () => import('@/views/exec/exec-template/index.vue'),
name: 'execCommand',
path: '/exec-command',
component: () => import('@/views/exec/exec-command/index.vue'),
},
{
name: 'execLog',
@@ -17,9 +17,9 @@ const EXEC: AppRouteRecordRaw = {
component: () => import('@/views/exec/exec-log/index.vue'),
},
{
name: 'execHostLog',
path: '/exec-host-log',
component: () => import('@/views/exec/exec-host-log/index.vue'),
name: 'execTemplate',
path: '/exec-template',
component: () => import('@/views/exec/exec-template/index.vue'),
},
],
};

View File

@@ -0,0 +1,17 @@
<template>
<div>123</div>
</template>
<script lang="ts">
export default {
name: 'execCommand'
};
</script>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,33 @@
import type { FieldRule } from '@arco-design/web-vue';
export const description = [{
maxLength: 128,
message: '执行描述长度不能大于128位'
}] as FieldRule[];
export const hostIdList = [{
required: true,
message: '请选择执行主机'
}] as FieldRule[];
export const command = [{
required: true,
message: '请输入执行命令'
}] as FieldRule[];
export const timeout = [{
required: true,
message: '请输入超时时间'
}, {
type: 'number',
min: 0,
max: 100000,
message: '超时时间需要在 0 - 100000 之间'
}] as FieldRule[];
export default {
description,
hostIdList,
command,
timeout,
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -58,7 +58,7 @@
日志
</a-button>
<!-- 中断 -->
<a-popconfirm content="确认要中断命令吗?"
<a-popconfirm content="确认要中断命令吗, 删除后会中断执行?"
position="left"
type="warning"
@ok="interruptedHost(record)">

View File

@@ -0,0 +1,162 @@
<template>
<a-modal v-model:visible="visible"
body-class="modal-form"
title-align="start"
title="清空执行记录"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
ok-text="清空"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin class="full" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:label-col-props="{ span: 5 }"
:wrapper-col-props="{ span: 18 }">
<!-- 执行用户 -->
<a-form-item field="userId" label="执行用户">
<user-selector v-model="formModel.userId"
placeholder="请选择执行用户"
allow-clear />
</a-form-item>
<!-- 执行描述 -->
<a-form-item field="description" label="执行描述">
<a-input v-model="formModel.description"
placeholder="请输入执行描述"
allow-clear />
</a-form-item>
<!-- 执行命令 -->
<a-form-item field="command" label="执行命令">
<a-input v-model="formModel.command"
placeholder="请输入执行命令"
allow-clear />
</a-form-item>
<!-- 执行状态 -->
<a-form-item field="status" label="执行状态">
<a-select v-model="formModel.status"
:options="toOptions(execStatusKey)"
placeholder="请选择执行状态" />
</a-form-item>
<!-- 执行时间 -->
<a-form-item field="startTimeRange" label="执行时间">
<a-range-picker v-model="formModel.startTimeRange"
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
show-time
format="YYYY-MM-DD HH:mm:ss" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'execLogClearModal'
};
</script>
<script lang="ts" setup>
import type { ExecLogQueryRequest } from '@/api/exec/exec-log';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { execStatusKey } from '../types/const';
import { getExecLogCount, clearExecLog } from '@/api/exec/exec-log';
import { Message, Modal } from '@arco-design/web-vue';
import { useDictStore } from '@/store';
import UserSelector from '@/components/user/user/selector/index.vue';
const emits = defineEmits(['clear']);
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const { toOptions } = useDictStore();
const formRef = ref<any>();
const formModel = ref<ExecLogQueryRequest>({});
const defaultForm = (): ExecLogQueryRequest => {
return {
id: undefined,
userId: undefined,
description: undefined,
command: undefined,
status: undefined,
startTimeRange: undefined
};
};
// 打开
const open = (record: any) => {
renderForm({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
formModel.value = Object.assign({}, record);
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 获取总数量
const { data } = await getExecLogCount(formModel.value);
if (data) {
// 清空
doClear(data);
} else {
// 无数据
Message.warning('当前条件未查询到数据');
}
} catch (e) {
} finally {
setLoading(false);
}
return false;
};
// 执行删除
const doClear = (count: number) => {
Modal.confirm({
title: '删除清空',
content: `确定要删除 ${count} 条数据吗? 确定后将立即删除且无法恢复!`,
onOk: async () => {
setLoading(true);
try {
// 调用删除
await clearExecLog(formModel.value);
emits('clear');
// 清空
setVisible(false);
handlerClear();
} catch (e) {
} finally {
setLoading(false);
}
}
});
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -61,8 +61,26 @@
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 执行命令 -->
<a-button v-permission="['asset:exec:exec-command']"
type="primary"
@click="$router.push({ name: 'execCommand' })">
执行命令
<template #icon>
<icon-thunderbolt />
</template>
</a-button>
<!-- 清空 -->
<a-button v-permission="['infra:exec-log:clear']"
status="danger"
@click="openClear">
清空
<template #icon>
<icon-close />
</template>
</a-button>
<!-- 删除 -->
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗?`"
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗? 删除后会中断执行!`"
position="br"
type="warning"
@ok="deleteSelectRows">
@@ -128,7 +146,7 @@
<a-popconfirm content="确定要重新执行吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
@ok="doReExecCommand(record)">
<a-button v-permission="['asset:exec:exec-command']"
type="text"
size="mini">
@@ -136,14 +154,13 @@
</a-button>
</a-popconfirm>
<!-- 命令 -->
<a-button v-permission="['asset:exec:interrupt-exec']"
type="text"
<a-button type="text"
size="mini"
@click="emits('viewCommand', record.command)">
命令
</a-button>
<!-- 日志 -->
<a-button v-permission="['asset:exec:interrupt-exec']"
<a-button v-permission="['asset:exec:exec-command']"
type="text"
size="mini"
@click="emits('viewLog', record.id)">
@@ -153,7 +170,7 @@
<a-popconfirm content="确定要中断执行吗?"
position="left"
type="warning"
@ok="interruptedExec(record)">
@ok="doInterruptExec(record)">
<a-button v-permission="['asset:exec:interrupt-exec']"
type="text"
size="mini"
@@ -163,7 +180,7 @@
</a-button>
</a-popconfirm>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
<a-popconfirm content="确认删除这条记录吗, 删除后会中断执行?"
position="left"
type="warning"
@ok="deleteRow(record)">
@@ -188,8 +205,8 @@
<script lang="ts" setup>
import type { ExecLogQueryRequest, ExecLogQueryResponse } from '@/api/exec/exec-log';
import { reactive, ref, onMounted } from 'vue';
import { batchDeleteExecLog, deleteExecLog, getExecHostLogList, getExecLogPage } from '@/api/exec/exec-log';
import { reactive, ref, onMounted, onUnmounted } from 'vue';
import { batchDeleteExecLog, deleteExecLog, getExecHostLogList, getExecLogPage, getExecLogStatus } from '@/api/exec/exec-log';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
@@ -197,13 +214,13 @@
import { useExpandable, usePagination, useRowSelection } from '@/types/table';
import { useDictStore } from '@/store';
import { dateFormat, formatDuration } from '@/utils';
import { interruptExec } from '@/api/exec/exec';
import { interruptExec, reExecCommand } from '@/api/exec/exec';
import UserSelector from '@/components/user/user/selector/index.vue';
import ExecHostLogTable from './exec-host-log-table.vue';
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog']);
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
// TODO 日志 清理 轮询状态 ctrl日志 ctrl重新执行
// TODO 日志 清理 ctrl日志 ctrl重新执行
const pagination = usePagination();
const rowSelection = useRowSelection();
@@ -211,6 +228,7 @@
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue } = useDictStore();
const intervalId = ref();
const tableRef = ref();
const selectedKeys = ref<number[]>([]);
const tableRenderData = ref<ExecLogQueryResponse[]>([]);
@@ -223,6 +241,11 @@
startTimeRange: undefined,
});
// 打开清理
const openClear = () => {
emits('openClear', { ...formModel, id: undefined });
};
// 删除选中行
const deleteSelectRows = async () => {
try {
@@ -256,8 +279,24 @@
}
};
// 重新执行命令
const doReExecCommand = async (record: ExecLogQueryResponse) => {
try {
setLoading(true);
// 调用中断接口
await reExecCommand({
logId: record.id
});
Message.success('已重新执行');
fetchTableData();
} catch (e) {
} finally {
setLoading(false);
}
};
// 中断执行
const interruptedExec = async (record: ExecLogQueryResponse) => {
const doInterruptExec = async (record: ExecLogQueryResponse) => {
try {
setLoading(true);
// 调用中断接口
@@ -282,6 +321,43 @@
record.hosts = data;
};
// 加载状态
const fetchTaskStatus = async () => {
const unCompleteIdList = tableRenderData.value
.filter(s => s.status === execStatus.WAITING || s.status === execStatus.RUNNING)
.map(s => s.id);
if (!unCompleteIdList.length) {
return;
}
// 加载未完成的状态
const { data: { logList, hostList } } = await getExecLogStatus(unCompleteIdList);
// 设置任务状态
logList.forEach(s => {
const tableRow = tableRenderData.value.find(r => r.id === s.id);
if (!tableRow) {
return;
}
tableRow.status = s.status;
tableRow.startTime = s.startTime;
tableRow.finishTime = s.finishTime;
});
// 设置主机状态
hostList.forEach(s => {
const host = tableRenderData.value
.find(r => r.id === s.logId)
?.hosts
?.find(r => r.id === s.id);
if (!host) {
return;
}
host.status = s.status;
host.startTime = s.startTime;
host.finishTime = s.finishTime;
host.exitStatus = s.exitStatus;
host.errorMessage = s.errorMessage;
});
};
// 加载数据
const doFetchTableData = async (request: ExecLogQueryRequest) => {
try {
@@ -304,8 +380,20 @@
doFetchTableData({ page, limit, ...form });
};
defineExpose({
fetchTableData
});
onMounted(() => {
// 加载数据
fetchTableData();
// 注册状态轮询
intervalId.value = setInterval(fetchTaskStatus, 10000);
});
onUnmounted(() => {
// 卸载状态轮询
clearInterval(intervalId.value);
});
</script>

View File

@@ -1,8 +1,13 @@
<template>
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<exec-log-table @view-command="viewCommand"
@view-params="viewParams" />
<exec-log-table ref="tableRef"
@view-command="viewCommand"
@view-params="viewParams"
@open-clear="openClearModal" />
<!-- 清理模态框 -->
<exec-log-clear-modal ref="clearModal"
@clear="clearCallback" />
<!-- json 模态框 -->
<json-editor-modal ref="jsonModal"
:esc-to-close="true" />
@@ -24,13 +29,21 @@
import { useDictStore } from '@/store';
import { dictKeys } from './types/const';
import ExecLogTable from './components/exec-log-table.vue';
import ExecLogClearModal from './components/exec-log-clear-modal.vue';
import JsonEditorModal from '@/components/view/json-editor/modal/index.vue';
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
const render = ref(false);
const tableRef = ref();
const clearModal = ref();
const jsonModal = ref();
const shellModal = ref();
// 打开清理模态框
const openClearModal = (e: any) => {
clearModal.value.open(e);
};
// 查看命令
const viewCommand = (data: string) => {
shellModal.value.open(data, '命令');
@@ -41,6 +54,11 @@
jsonModal.value.open(JSON.parse(data));
};
// 清理回调
const clearCallback = () => {
tableRef.value.fetchTableData();
};
onBeforeMount(async () => {
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);

View File

@@ -0,0 +1,223 @@
<template>
<a-drawer v-model:visible="visible"
title="执行命令"
:width="470"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@cancel="handleClose">
<a-spin class="full spin-wrapper" :loading="loading">
<!-- 命令表单 -->
<a-form :model="formModel"
ref="formRef"
:rules="formRules">
<!-- 执行主机 -->
<a-form-item field="hostIdList" label="执行主机">
<div class="selected-host">
<!-- 已选择数量 -->
<span class="usn" v-if="formModel.hostIdList?.length">
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
</span>
<span class="usn pointer span-blue" @click="openSelectHost">
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
</span>
</div>
</a-form-item>
<!-- 执行描述 -->
<a-form-item field="description" label="执行描述">
<a-input v-model="formModel.description"
placeholder="请输入执行描述"
allow-clear />
</a-form-item>
<!-- 超时时间 -->
<a-form-item field="timeout"
label="超时时间">
<a-input-number v-model="formModel.timeout"
placeholder="为0则不超时"
:min="0"
:max="100000"
hide-button>
<template #suffix>
</template>
</a-input-number>
</a-form-item>
<!-- 模板命令 -->
<a-form-item field="command"
label="模板命令"
:wrapper-col-props="{ span: 24 }">
<exec-editor v-model="formModel.command"
containerClass="command-editor"
theme="vs-dark"
:parameter="parameterSchema" />
</a-form-item>
</a-form>
<!-- 命令参数 -->
<a-divider v-if="parameterSchema.length"
orientation="center"
style="margin: 12px 0 26px 0;">
命令参数
</a-divider>
<!-- 参数表单 -->
<a-form v-if="parameterSchema.length"
:model="parameterFormModel"
ref="parameterFormRef"
label-align="right"
:label-col-props="{ span: 5 }"
:wrapper-col-props="{ span: 18 }">
<a-form-item v-for="item in parameterSchema"
:key="item.name"
:field="item.name as string"
:label="item.name"
required>
<a-input v-model="parameterFormModel[item.name]"
:placeholder="item.desc"
allow-clear />
</a-form-item>
</a-form>
</a-spin>
<!-- 主机模态框 -->
<authorized-host-modal ref="hostModal"
@selected="setSelectedHost" />
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'execTemplateExecDrawer'
};
</script>
<script lang="ts" setup>
import type { TemplateParam } from '@/components/view/exec-editor/const';
import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template';
import type { ExecCommandRequest } from '@/api/exec/exec';
import { onUnmounted, ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../../exec-command/types/form.rules';
import { Message } from '@arco-design/web-vue';
import ExecEditor from '@/components/view/exec-editor/index.vue';
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
import { execCommand } from '@/api/exec/exec';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const formRef = ref<any>();
const parameterFormRef = ref<any>();
const hostModal = ref<any>();
const formModel = ref<ExecCommandRequest>({});
const parameterFormModel = ref<Record<string, any>>({});
const parameterSchema = ref<Array<TemplateParam>>([]);
// 打开
const open = (record: ExecTemplateQueryResponse) => {
renderForm({ ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: ExecTemplateQueryResponse) => {
formModel.value = {
description: record.name,
timeout: record.timeout,
command: record.command,
hostIdList: []
};
if (record.parameter) {
parameterSchema.value = JSON.parse(record.parameter);
const params = {} as any;
for (let param of parameterSchema.value) {
params[param.name as keyof any] = param.default;
}
parameterFormModel.value = params;
} else {
parameterSchema.value = [];
parameterFormModel.value = {};
}
};
defineExpose({ open });
// 打开选择主机
const openSelectHost = () => {
hostModal.value.open(formModel.value.hostIdList);
};
// 设置选中主机
const setSelectedHost = (hosts: Array<number>) => {
formModel.value.hostIdList = hosts;
};
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
let error = await formRef.value.validate();
if (error) {
return false;
}
error = await parameterFormRef.value?.validate();
if (error) {
return false;
}
await execCommand({
...formModel.value,
parameter: JSON.stringify(parameterFormModel.value),
parameterSchema: JSON.stringify(parameterSchema.value),
});
Message.success('已开始执行');
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
// 卸载关闭
onUnmounted(handlerClear);
</script>
<style lang="less" scoped>
.spin-wrapper {
padding: 16px 20px;
}
.selected-host {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
&-count {
font-size: 16px;
font-weight: 600;
display: inline-block;
margin: 0 6px;
}
}
.command-editor {
width: 100%;
height: 44vh;
}
</style>

View File

@@ -11,7 +11,6 @@
<a-spin class="full modal-form" :loading="loading">
<a-form :model="formModel"
ref="formRef"
layout="vertical"
:rules="formRules">
<!-- 模板名称 -->
<a-form-item field="name" label="模板名称">
@@ -33,17 +32,20 @@
</a-input-number>
</a-form-item>
<!-- 模板命令 -->
<a-form-item field="command" label="模板命令">
<editor v-model="formModel.command"
containerClass="command-editor"
language="shell"
theme="vs-dark"
:suggestions="false" />
<a-form-item field="command"
label="模板命令"
:wrapper-col-props="{ span: 24 }">
<exec-editor v-model="formModel.command"
containerClass="command-editor"
theme="vs-dark"
:parameter="parameter" />
</a-form-item>
<!-- 命令参数 -->
<a-form-item field="parameter"
class="parameter-form-item"
label="命令参数">
label="命令参数"
:label-col-props="{ span: 24 }"
:wrapper-col-props="{ span: 24 }">
<!-- label -->
<template #label>
<div class="parameter-label-wrapper">
@@ -72,8 +74,8 @@
<span class="parameter-item-close click-icon-wrapper"
title="移除"
@click="removeParameter(i)">
<icon-close />
</span>
<icon-close />
</span>
</a-input-group>
</template>
<!-- 无参数 -->
@@ -95,17 +97,15 @@
</script>
<script lang="ts" setup>
import type { TemplateParam } from '../types/const';
import type { TemplateParam } from '@/components/view/exec-editor/const';
import type { ExecTemplateUpdateRequest } from '@/api/exec/exec-template';
import { onUnmounted, ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { createExecTemplate, updateExecTemplate } from '@/api/exec/exec-template';
import { builtinsParams } from '../types/const';
import { Message } from '@arco-design/web-vue';
import * as monaco from 'monaco-editor';
import { language as shellLanguage } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
import ExecEditor from '@/components/view/exec-editor/index.vue';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
@@ -126,7 +126,6 @@
const formRef = ref<any>();
const formModel = ref<ExecTemplateUpdateRequest>({});
const parameter = ref<Array<TemplateParam>>([]);
const suggestionsDispose = ref();
const emits = defineEmits(['added', 'updated']);
@@ -154,8 +153,6 @@
} else {
parameter.value = [];
}
// 注册代码提示
registerSuggestions();
};
defineExpose({ openAdd, openUpdate });
@@ -170,57 +167,6 @@
parameter.value.splice(index, 1);
};
// 注册代码提示
const registerSuggestions = () => {
if (suggestionsDispose.value) {
return;
}
// 代码提示
suggestionsDispose.value = monaco.languages.registerCompletionItemProvider('shell', {
provideCompletionItems() {
const suggestions: any = [];
shellLanguage.keywords?.forEach((item: any) => {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item
});
});
shellLanguage.builtins?.forEach((item: any) => {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Function,
insertText: item,
});
});
// 内置参数提示
builtinsParams.forEach(s => {
suggestions.push({
label: s.name,
kind: monaco.languages.CompletionItemKind.Function,
insertText: `@{{ ${s.name} }}`,
detail: s.desc || '',
});
});
// 命令参数提示
parameter.value.forEach(s => {
if (!s.name) {
return;
}
suggestions.push({
label: s.name,
kind: monaco.languages.CompletionItemKind.Function,
insertText: `@{{ ${s.name} }}`,
detail: s.desc || '',
});
});
return {
suggestions: [...new Set(suggestions)],
};
},
});
};
// 确定
const handlerOk = async () => {
setLoading(true);
@@ -268,9 +214,6 @@
// 清空
const handlerClear = () => {
setLoading(false);
// 卸载代码提示
suggestionsDispose.value?.dispose();
suggestionsDispose.value = undefined;
};
// 卸载关闭
@@ -352,7 +295,7 @@
.command-editor {
width: 100%;
height: 44vh;
height: 50vh;
}
</style>

View File

@@ -76,6 +76,12 @@
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<a-button v-permission="['asset:exec:exec-command']"
type="text"
size="mini"
@click="emits('openExec', record)">
执行
</a-button>
<!-- 修改 -->
<a-button v-permission="['asset:exec-template:update']"
type="text"
@@ -118,14 +124,13 @@
import { usePagination } from '@/types/table';
import useCopy from '@/hooks/copy';
const emits = defineEmits(['openAdd', 'openUpdate']);
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
const emits = defineEmits(['openAdd', 'openUpdate', 'openExec']);
const pagination = usePagination();
const { loading, setLoading } = useLoading();
const { copy } = useCopy();
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
const formModel = reactive<ExecTemplateQueryRequest>({
id: undefined,
name: undefined,

View File

@@ -2,12 +2,15 @@
<div class="layout-container" v-if="render">
<!-- 列表-表格 -->
<exec-template-table ref="table"
@openAdd="() => drawer.openAdd()"
@openUpdate="(e) => drawer.openUpdate(e)" />
@open-exec="e => execModal.open(e)"
@openAdd="() => drawer.openAdd()"
@openUpdate="(e) => drawer.openUpdate(e)" />
<!-- 添加修改模态框 -->
<exec-template-form-drawer ref="drawer"
@added="modalAddCallback"
@updated="modalUpdateCallback" />
@added="modalAddCallback"
@updated="modalUpdateCallback" />
<!-- 执行模态框 -->
<exec-template-exec-drawer ref="execModal" />
</div>
</template>
@@ -21,10 +24,12 @@
import { ref, onBeforeMount } from 'vue';
import ExecTemplateTable from './components/exec-template-table.vue';
import ExecTemplateFormDrawer from './components/exec-template-form-drawer.vue';
import ExecTemplateExecDrawer from './components/exec-template-exec-drawer.vue';
const render = ref(false);
const table = ref();
const drawer = ref();
const execModal = ref();
// 添加回调
const modalAddCallback = () => {

View File

@@ -1,56 +0,0 @@
// 模板参数
export interface TemplateParam {
name?: string;
default?: string;
desc?: string;
}
// 内置参数
export const builtinsParams: Array<TemplateParam> = [
{
name: 'hostId',
desc: '执行主机id'
}, {
name: 'hostName',
desc: '执行主机名称'
}, {
name: 'hostCode',
desc: '执行主机编码'
}, {
name: 'hostAddress',
desc: '执行主机地址'
}, {
name: 'userId',
desc: '执行用户id'
}, {
name: 'username',
desc: '执行用户名'
}, {
name: 'execId',
desc: '执行记录id'
}, {
name: 'uuid',
desc: '生成任务维度 uuid'
}, {
name: 'uuidShort',
desc: '生成任务维度 uuid 无 \'-\''
}, {
name: 'hostUuid',
desc: '生成机器维度 uuid'
}, {
name: 'hostUuidShort',
desc: '生成机器维度 uuid 无 \'-\''
}, {
name: 'timestampMillis',
desc: '时间戳毫秒'
}, {
name: 'timestamp',
desc: '时间戳'
}, {
name: 'date',
desc: '执行时间 yyyy-MM-dd'
}, {
name: 'datetime',
desc: '执行时间 yyyy-MM-dd HH:mm:ss'
},
];

View File

@@ -16,6 +16,11 @@ export const command = [{
export const timeout = [{
required: true,
message: '请输入超时时间'
}, {
type: 'number',
min: 0,
max: 100000,
message: '超时时间需要在 0 - 100000 之间'
}] as FieldRule[];
export default {