🚀 feat(server): SSH 脚本支持引用全局脚本库

This commit is contained in:
bwcx_jzy 2024-06-16 18:00:51 +08:00
parent 3cfc8e8c05
commit 5741243777
8 changed files with 786 additions and 11 deletions

View File

@ -4,7 +4,7 @@
### 🐣 新增功能
1. 【server】新增 服务端脚本支持引用全局脚本库(`G@("xx")` xx 为脚本标记)
1. 【server】新增 服务端脚本、SSH 脚本支持引用全局脚本库(`G@("xx")` xx 为脚本标记)
### 🐞 解决BUG、优化功能

View File

@ -9,7 +9,6 @@
*/
package org.dromara.jpom.service.node.ssh;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
@ -24,6 +23,7 @@ import org.dromara.jpom.common.i18n.I18nMessageUtil;
import org.dromara.jpom.common.i18n.I18nThreadUtil;
import org.dromara.jpom.cron.CronUtils;
import org.dromara.jpom.func.assets.model.MachineSshModel;
import org.dromara.jpom.func.assets.server.ScriptLibraryServer;
import org.dromara.jpom.model.EnvironmentMapBuilder;
import org.dromara.jpom.model.data.CommandExecLogModel;
import org.dromara.jpom.model.data.CommandModel;
@ -60,15 +60,18 @@ public class SshCommandService extends BaseWorkspaceService<CommandModel> implem
private final SshService sshService;
private final CommandExecLogService commandExecLogService;
private final WorkspaceEnvVarService workspaceEnvVarService;
private final ScriptLibraryServer scriptLibraryServer;
private static final byte[] LINE_BYTES = SystemUtil.getOsInfo().getLineSeparator().getBytes(CharsetUtil.CHARSET_UTF_8);
public SshCommandService(SshService sshService,
CommandExecLogService commandExecLogService,
WorkspaceEnvVarService workspaceEnvVarService) {
WorkspaceEnvVarService workspaceEnvVarService,
ScriptLibraryServer scriptLibraryServer) {
this.sshService = sshService;
this.commandExecLogService = commandExecLogService;
this.workspaceEnvVarService = workspaceEnvVarService;
this.scriptLibraryServer = scriptLibraryServer;
}
@Override
@ -273,7 +276,8 @@ public class SshCommandService extends BaseWorkspaceService<CommandModel> implem
environmentMapBuilder.eachStr(logRecorder::system);
Map<String, String> environment = environmentMapBuilder.environment();
String commands = StringUtil.formatStrByMap(commandModel.getCommand(), environment);
// 替换全局脚本
commands = scriptLibraryServer.referenceReplace(commands);
MachineSshModel machineSshModel = sshService.getMachineSshModel(sshModel);
//
Session session = null;

View File

@ -108,6 +108,7 @@ public class ScriptExecuteLogServer extends BaseGlobalOrWorkspaceService<ScriptE
InputStream templateInputStream = ExtConfigBean.getConfigResourceInputStream("/exec/template." + CommandUtil.SUFFIX);
String defaultTemplate = IoUtil.readUtf8(templateInputStream);
String context = defaultTemplate + scriptModel.getContext();
// 替换全局变量
context = scriptLibraryServer.referenceReplace(context);
//
String dataPath = jpomApplication.getDataPath();

View File

@ -0,0 +1,36 @@
<template>
<a-drawer v-bind="props" :root-style="rootStyle">
<slot name="default"></slot>
<template v-if="slots.footer" #footer>
<slot name="footer"></slot>
</template>
<template v-if="slots.extra" #extra>
<slot name="extra"></slot>
</template>
</a-drawer>
</template>
<script lang="ts">
import { drawerProps } from 'ant-design-vue/es/drawer'
import { initDefaultProps } from 'ant-design-vue/es/_util/props-util'
import { CustomSlotsType } from 'ant-design-vue/es/_util/type'
import { CSSProperties, defineComponent } from 'vue'
import { increaseZIndex } from '@/utils/utils'
export default defineComponent({
name: 'CustomDrawer',
props: initDefaultProps(drawerProps(), {}),
slots: Object as CustomSlotsType<{ default: any; footer?: any; extra?: any }>,
setup(props, { emit, slots }) {
const rootStyle: CSSProperties = {
zIndex: increaseZIndex(),
...props.rootStyle
}
return {
props,
rootStyle,
emit,
slots
}
}
})
</script>

View File

@ -430,7 +430,7 @@
"
@tag-confirm="
(tag) => {
temp = { ...temp, context: temp.context + `\nG@(\&quot;${tag}\&quot;)` }
temp = { ...temp, context: (temp.context || '') + `\nG@(\&quot;${tag}\&quot;)` }
scriptLibraryVisible = false
}
"

View File

@ -114,7 +114,8 @@
</template>
</CustomTable>
<!-- 编辑命令 -->
<a-modal
<CustomModal
v-if="editCommandVisible"
v-model:open="editCommandVisible"
destroy-on-close
width="80vw"
@ -146,7 +147,12 @@
v-model:content="temp.command"
height="40vh"
:options="{ mode: 'shell', tabSize: 2 }"
></code-editor>
:show-tool="true"
>
<template #tool_before>
<a-button type="link" @click="scriptLibraryVisible = true">脚本库 </a-button>
</template>
</code-editor>
</a-form-item-rest>
</a-form-item>
<a-form-item :label="$t('pages.ssh.command.9266c899')">
@ -228,7 +234,7 @@
/>
</a-form-item>
</a-form>
</a-modal>
</CustomModal>
<a-modal
v-model:open="executeCommandVisible"
@ -417,6 +423,71 @@
</a-tabs>
</a-form>
</a-modal>
<!-- 查看脚本库 -->
<CustomDrawer
v-if="scriptLibraryVisible"
destroy-on-close
title="查看脚本库"
placement="right"
:open="scriptLibraryVisible"
width="85vw"
:footer-style="{ textAlign: 'right' }"
@close="
() => {
scriptLibraryVisible = false
}
"
>
<ScriptLibraryNoPermission
v-if="scriptLibraryVisible"
ref="scriptLibraryRef"
@script-confirm="
(script) => {
temp = { ...temp, command: script }
scriptLibraryVisible = false
}
"
@tag-confirm="
(tag) => {
temp = { ...temp, command: (temp.command || '') + `\nG@(\&quot;${tag}\&quot;)` }
scriptLibraryVisible = false
}
"
></ScriptLibraryNoPermission>
<template #footer>
<a-space>
<a-button
@click="
() => {
scriptLibraryVisible = false
}
"
>
取消
</a-button>
<a-button
type="primary"
@click="
() => {
$refs['scriptLibraryRef'].handerScriptConfirm()
}
"
>
替换引用
</a-button>
<a-button
type="primary"
@click="
() => {
$refs['scriptLibraryRef'].handerTagConfirm()
}
"
>
标记引用
</a-button>
</a-space>
</template>
</CustomDrawer>
</div>
</template>
<script>
@ -426,12 +497,12 @@ import { CRON_DATA_SOURCE } from '@/utils/const-i18n'
import { getSshListAll } from '@/api/ssh'
import codeEditor from '@/components/codeEditor'
import CommandLog from './command-view-log'
import ScriptLibraryNoPermission from '@/pages/system/assets/script-library/no-permission'
import { getWorkSpaceListAll } from '@/api/workspace'
import { mapState } from 'pinia'
import { useAppStore } from '@/stores/app'
export default {
components: { codeEditor, CommandLog },
components: { codeEditor, CommandLog, ScriptLibraryNoPermission },
data() {
return {
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
@ -503,7 +574,7 @@ export default {
width: '240px'
}
],
scriptLibraryVisible: false,
tableSelections: [],
syncToWorkspaceVisible: false,
workspaceList: [],

View File

@ -0,0 +1,277 @@
<template>
<div>
<!-- 数据表格 -->
<CustomTable
is-show-tools
default-auto-refresh
:auto-refresh-time="30"
:active-page="activePage"
table-name="script-library-no-permission"
empty-description="没有任何脚本库"
:data-source="list"
size="middle"
:columns="columns"
:pagination="pagination"
bordered
row-key="id"
:scroll="{
x: 'max-content'
}"
:row-selection="rowSelection"
@change="changePage"
@refresh="loadData"
>
<template #title>
<a-space wrap class="search-box">
<a-input
v-model:value="listQuery['%tag%']"
placeholder="脚本标记"
allow-clear
class="search-input-item"
@press-enter="loadData"
/>
<a-input
v-model:value="listQuery['%version%']"
placeholder="版本"
allow-clear
class="search-input-item"
@press-enter="loadData"
/>
<a-input
v-model:value="listQuery['%description%']"
placeholder="描述"
class="search-input-item"
@press-enter="loadData"
/>
<a-tooltip title="按住 Ctr 或者 Alt/Option 键点击按钮快速回到第一页">
<a-button :loading="loading" type="primary" @click="loadData">搜索</a-button>
</a-tooltip>
</a-space>
</template>
<template #tableBodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'nodeId'">
<a-tooltip placement="topLeft" :title="text">
<span>{{ nodeMap[text] }}</span>
</a-tooltip>
</template>
<template v-else-if="column.tooltip">
<a-tooltip placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-space>
<a-button size="small" type="primary" @click="handleEdit(record)"> 查看</a-button>
</a-space>
</template>
</template>
</CustomTable>
<!-- pages.system.assets.script-library.ad207008区 -->
<CustomModal
v-if="editScriptVisible"
v-model:open="editScriptVisible"
destroy-on-close
title="查看脚本"
:mask-closable="false"
width="80vw"
:footer="false"
>
<a-form ref="editScriptForm" :rules="rules" :model="temp" :label-col="{ span: 3 }" :wrapper-col="{ span: 19 }">
<a-form-item label="版本" name="id">
<a-input v-model:value="temp.version" disabled read-only />
</a-form-item>
<a-form-item label="标记" name="tag">
<a-input v-model:value="temp.tag" :max-length="50" disabled />
</a-form-item>
<a-form-item label="内容" name="script">
<a-form-item-rest>
<code-editor
v-model:content="temp.script"
:show-tool="true"
height="40vh"
:options="{ mode: 'shell', tabSize: 2, readOnly: true }"
>
</code-editor>
</a-form-item-rest>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="temp.description" :max-length="200" :rows="3" style="resize: none" disabled />
</a-form-item>
</a-form>
</CustomModal>
</div>
</template>
<script>
import { getScriptLibraryList } from '@/api/system/script-library'
import codeEditor from '@/components/codeEditor'
import { CRON_DATA_SOURCE } from '@/utils/const-i18n'
import { CHANGE_PAGE, COMPUTED_PAGINATION, PAGE_DEFAULT_LIST_QUERY, parseTime } from '@/utils/const'
import { increaseZIndex } from '@/utils/utils'
// import { getWorkSpaceListAll } from '@/api/workspace'
export default {
components: {
codeEditor
},
props: {},
emits: ['scriptConfirm', 'tagConfirm'],
data() {
return {
// choose: this.choose,
loading: false,
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
CRON_DATA_SOURCE,
list: [],
temp: {},
nodeList: [],
editScriptVisible: false,
drawerTitle: '',
drawerConsoleVisible: false,
columns: [
{
title: '标记',
dataIndex: 'tag',
ellipsis: true,
sorter: true,
width: 150
},
{
title: '版本',
dataIndex: 'version',
ellipsis: true,
sorter: true,
width: '100px',
tooltip: true
},
{
title: '描述',
dataIndex: 'description',
ellipsis: true,
width: 200,
tooltip: true
},
{
title: '修改时间',
dataIndex: 'modifyTimeMillis',
sorter: true,
width: '170px',
ellipsis: true,
customRender: ({ text }) => parseTime(text)
},
{
title: '创建时间',
dataIndex: 'createTimeMillis',
sorter: true,
width: '170px',
ellipsis: true,
customRender: ({ text }) => parseTime(text)
},
{
title: '创建人',
dataIndex: 'createUser',
ellipsis: true,
tooltip: true,
width: '120px'
},
{
title: '修改人',
dataIndex: 'modifyUser',
ellipsis: true,
tooltip: true,
width: '120px'
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: '140px'
}
],
tableSelections: [],
selectedRowKeys: [],
rules: {}
}
},
computed: {
pagination() {
return COMPUTED_PAGINATION(this.listQuery)
},
activePage() {
return this.$attrs.routerUrl === this.$route.path
},
rowSelection() {
return {
onChange: (selectedRowKeys) => {
this.tableSelections = selectedRowKeys
},
selectedRowKeys: this.tableSelections,
type: 'radio'
}
}
},
watch: {},
created() {},
mounted() {
this.loadData()
},
methods: {
increaseZIndex,
//
loadData(pointerEvent) {
this.listQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.listQuery.page
this.loading = true
getScriptLibraryList(this.listQuery).then((res) => {
if (res.code === 200) {
this.list = res.data.result
this.listQuery.total = res.data.total
}
this.loading = false
})
},
parseTime,
//
handleEdit(record) {
this.temp = Object.assign({}, record)
this.editScriptVisible = true
},
//
changePage(pagination, filters, sorter) {
this.listQuery = CHANGE_PAGE(this.listQuery, { pagination, sorter })
this.loadData()
},
handerScriptConfirm() {
if (!this.tableSelections.length) {
$notification.warning({
message: '请选择要引用的脚本'
})
return
}
const selectData = this.list.filter((item) => {
return item.id === this.tableSelections[0]
})?.[0]
this.$emit('scriptConfirm', `${selectData.script}`)
},
handerTagConfirm() {
if (!this.tableSelections.length) {
$notification.warning({
message: '请选择要引用的脚本'
})
return
}
const selectData = this.list.filter((item) => {
return item.id === this.tableSelections[0]
})?.[0]
this.$emit('tagConfirm', `${selectData.tag}`)
}
}
}
</script>

View File

@ -0,0 +1,386 @@
<template>
<div>
<!-- 数据表格 -->
<CustomTable
is-show-tools
default-auto-refresh
:auto-refresh-time="30"
:active-page="activePage"
table-name="script-library"
:empty-description="$t('pages.system.assets.script-library.65418a5e')"
:data-source="list"
size="middle"
:columns="columns"
:pagination="pagination"
bordered
row-key="id"
:scroll="{
x: 'max-content'
}"
@change="changePage"
@refresh="loadData"
>
<template #title>
<a-space wrap class="search-box">
<a-input
v-model:value="listQuery['%tag%']"
:placeholder="$t('pages.system.assets.script-library.95547f9')"
allow-clear
class="search-input-item"
@press-enter="loadData"
/>
<a-input
v-model:value="listQuery['%version%']"
:placeholder="$t('pages.system.assets.script-library.81634069')"
allow-clear
class="search-input-item"
@press-enter="loadData"
/>
<a-input
v-model:value="listQuery['%description%']"
:placeholder="$t('pages.system.assets.script-library.f89e58f1')"
class="search-input-item"
@press-enter="loadData"
/>
<a-tooltip :title="$t('pages.system.assets.script-library.986e8dc2')">
<a-button :loading="loading" type="primary" @click="loadData">{{
$t('pages.system.assets.script-library.43934f6d')
}}</a-button>
</a-tooltip>
<a-button type="primary" @click="createScript">{{
$t('pages.system.assets.script-library.9f3089ce')
}}</a-button>
</a-space>
</template>
<template #tableHelp>
<a-tooltip>
<template #title>
<div>{{ $t('pages.system.assets.script-library.41c0cbe5') }}</div>
<div>
<ul>
<li>{{ $t('pages.system.assets.script-library.423e1405') }}</li>
</ul>
</div>
</template>
<QuestionCircleOutlined />
</a-tooltip>
</template>
<template #tableBodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'nodeId'">
<a-tooltip placement="topLeft" :title="text">
<span>{{ nodeMap[text] }}</span>
</a-tooltip>
</template>
<template v-else-if="column.tooltip">
<a-tooltip placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
</template>
<template v-else-if="column.dataIndex === 'operation'">
<a-space>
<a-button size="small" type="primary" @click="handleEdit(record)">{{
$t('pages.system.assets.script-library.ad207008')
}}</a-button>
<a-button size="small" type="primary" danger @click="handleDelete(record)">{{
$t('pages.system.assets.script-library.ecbd7449')
}}</a-button>
</a-space>
</template>
</template>
</CustomTable>
<!-- pages.system.assets.script-library.ad207008区 -->
<CustomModal
v-if="editScriptVisible"
v-model:open="editScriptVisible"
destroy-on-close
:title="$t('pages.system.assets.script-library.16a6aab6')"
:mask-closable="false"
width="80vw"
:confirm-loading="confirmLoading"
@ok="handleEditScriptOk"
>
<a-form ref="editScriptForm" :rules="rules" :model="temp" :label-col="{ span: 3 }" :wrapper-col="{ span: 19 }">
<a-form-item v-if="temp.id" :label="$t('pages.system.assets.script-library.81634069')" name="id">
<a-input v-model:value="temp.version" disabled read-only />
</a-form-item>
<a-form-item :label="$t('pages.system.assets.script-library.2d62ebdb')" name="tag">
<a-input
v-model:value="temp.tag"
:max-length="50"
:placeholder="$t('pages.system.assets.script-library.e37b1ac9')"
:disabled="!!temp.id"
/>
</a-form-item>
<a-form-item :label="$t('pages.system.assets.script-library.3e7aa0ad')" name="script">
<a-form-item-rest>
<code-editor
v-model:content="temp.script"
:show-tool="true"
height="40vh"
:options="{ mode: 'shell', tabSize: 2 }"
>
</code-editor>
</a-form-item-rest>
</a-form-item>
<a-form-item :label="$t('pages.system.assets.script-library.f89e58f1')" name="description">
<a-textarea
v-model:value="temp.description"
:max-length="200"
:rows="3"
style="resize: none"
:placeholder="$t('pages.system.assets.script-library.43075dd9')"
/>
</a-form-item>
<a-form-item>
<template #label>
<a-tooltip
>{{ $t('pages.system.assets.script-library.4f5ca5e3')
}}<template #title>{{ $t('pages.system.assets.script-library.33437c9b') }}</template>
<QuestionCircleOutlined v-show="!temp.id" />
</a-tooltip>
</template>
<template #help>{{ $t('pages.system.assets.script-library.5251812f') }}</template>
<a-select
v-model:value="temp.chooseNode"
show-search
:filter-option="false"
:placeholder="$t('pages.system.assets.script-library.4722ff63')"
mode="multiple"
@search="searchMachineList"
>
<a-select-option v-for="item in nodeList" :key="item.id" :value="item.id">
{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</CustomModal>
</div>
</template>
<script>
import { getScriptLibraryList, editScriptLibrary, delScriptLibrary } from '@/api/system/script-library'
import codeEditor from '@/components/codeEditor'
import { machineSearch } from '@/api/system/assets-machine'
import { CRON_DATA_SOURCE } from '@/utils/const-i18n'
import { CHANGE_PAGE, COMPUTED_PAGINATION, PAGE_DEFAULT_LIST_QUERY, parseTime } from '@/utils/const'
// import { getWorkSpaceListAll } from '@/api/workspace'
export default {
components: {
codeEditor
},
props: {},
data() {
return {
// choose: this.choose,
loading: false,
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
CRON_DATA_SOURCE,
list: [],
temp: {},
nodeList: [],
editScriptVisible: false,
drawerTitle: '',
drawerConsoleVisible: false,
columns: [
{
title: this.$t('pages.system.assets.script-library.2d62ebdb'),
dataIndex: 'tag',
ellipsis: true,
sorter: true,
width: 150
},
{
title: this.$t('pages.system.assets.script-library.81634069'),
dataIndex: 'version',
ellipsis: true,
sorter: true,
width: '100px',
tooltip: true
},
{
title: this.$t('pages.system.assets.script-library.f89e58f1'),
dataIndex: 'description',
ellipsis: true,
width: 200,
tooltip: true
},
{
title: this.$t('pages.system.assets.script-library.d3b29478'),
dataIndex: 'modifyTimeMillis',
sorter: true,
width: '170px',
ellipsis: true,
customRender: ({ text }) => parseTime(text)
},
{
title: this.$t('pages.system.assets.script-library.efaf9956'),
dataIndex: 'createTimeMillis',
sorter: true,
width: '170px',
ellipsis: true,
customRender: ({ text }) => parseTime(text)
},
{
title: this.$t('pages.system.assets.script-library.339d15b5'),
dataIndex: 'createUser',
ellipsis: true,
tooltip: true,
width: '120px'
},
{
title: this.$t('pages.system.assets.script-library.8605b4f2'),
dataIndex: 'modifyUser',
ellipsis: true,
tooltip: true,
width: '120px'
},
{
title: this.$t('pages.system.assets.script-library.fe731dfc'),
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: '140px'
}
],
rules: {
// name: [{ required: true, message: this.$tl('p.inputScriptName'), trigger: 'blur' }],
// context: [{ required: true, message: this.$tl('p.inputScriptContent'), trigger: 'blur' }]
},
confirmLoading: false
}
},
computed: {
pagination() {
return COMPUTED_PAGINATION(this.listQuery)
},
activePage() {
return this.$attrs.routerUrl === this.$route.path
}
},
watch: {},
created() {
// this.columns.push(
// );
},
mounted() {
// this.calcTableHeight();
this.loadData()
},
methods: {
//
loadData(pointerEvent) {
this.listQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.listQuery.page
this.loading = true
getScriptLibraryList(this.listQuery).then((res) => {
if (res.code === 200) {
this.list = res.data.result
this.listQuery.total = res.data.total
}
this.loading = false
})
},
parseTime,
//
searchMachineList(name) {
machineSearch({
name: name,
limit: 10,
appendIds: this.temp.machineIds || ''
}).then((res) => {
this.nodeList = res.data || []
})
},
createScript() {
this.temp = {}
this.editScriptVisible = true
this.searchMachineList()
},
//
handleEdit(record) {
this.temp = Object.assign({}, record)
//this.commandParams = data?.defArgs ? JSON.parse(data.defArgs) : []
this.temp = {
...this.temp,
chooseNode: record?.machineIds ? record.machineIds.split(',') : []
}
this.editScriptVisible = true
this.searchMachineList()
// getScriptItem({
// id: record.id
// }).then((res) => {
// if (res.code === 200) {
// const data = res.data.data
// }
// })
},
// Script
handleEditScriptOk() {
//
this.$refs['editScriptForm'].validate().then(() => {
//
this.temp.machineIds = this.temp?.chooseNode?.join(',')
delete this.temp.nodeList
this.confirmLoading = true
editScriptLibrary(this.temp)
.then((res) => {
if (res.code === 200) {
//
$notification.success({
message: res.msg
})
this.editScriptVisible = false
this.loadData()
this.$refs['editScriptForm'].resetFields()
}
})
.finally(() => {
this.confirmLoading = false
})
})
},
handleDelete(record) {
$confirm({
title: this.$t('pages.system.assets.script-library.3875bf60'),
content: this.$t('pages.system.assets.script-library.72df294d'),
zIndex: 1009,
okText: this.$t('pages.system.assets.script-library.d507abff'),
cancelText: this.$t('pages.system.assets.script-library.a0451c97'),
onOk: () => {
return delScriptLibrary({
id: record.id
}).then((res) => {
if (res.code === 200) {
$notification.success({
message: res.msg
})
this.loadData()
}
})
}
})
},
//
changePage(pagination, filters, sorter) {
this.listQuery = CHANGE_PAGE(this.listQuery, { pagination, sorter })
this.loadData()
}
}
}
</script>