From 9d6b46fcb28041ac393929fd6e28b07fec964f12 Mon Sep 17 00:00:00 2001 From: vapao Date: Wed, 30 Mar 2022 22:15:05 +0800 Subject: [PATCH] =?UTF-8?q?A=20=E6=89=B9=E9=87=8F=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8F=82=E6=95=B0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spug_api/apps/exec/executors.py | 4 +- spug_api/apps/exec/models.py | 10 ++- spug_api/apps/exec/views.py | 7 +- spug_web/src/pages/exec/task/Output.js | 10 +-- spug_web/src/pages/exec/task/Parameter.js | 65 ++++++++++++++++ spug_web/src/pages/exec/task/index.js | 26 +++++-- .../src/pages/exec/task/index.module.less | 11 ++- spug_web/src/pages/exec/template/Form.js | 78 +++++++++++++++---- spug_web/src/pages/exec/template/Parameter.js | 71 +++++++++++++++++ spug_web/src/pages/exec/template/store.js | 9 ++- 10 files changed, 255 insertions(+), 36 deletions(-) create mode 100644 spug_web/src/pages/exec/task/Parameter.js create mode 100644 spug_web/src/pages/exec/template/Parameter.js diff --git a/spug_api/apps/exec/executors.py b/spug_api/apps/exec/executors.py index 7a0d0b8..0122dcd 100644 --- a/spug_api/apps/exec/executors.py +++ b/spug_api/apps/exec/executors.py @@ -14,7 +14,7 @@ def exec_worker_handler(job): class Job: - def __init__(self, key, name, hostname, port, username, pkey, command, interpreter, token=None): + def __init__(self, key, name, hostname, port, username, pkey, command, interpreter, params=None, token=None): self.ssh = SSH(hostname, port, username, pkey) self.key = key self.command = self._handle_command(command, interpreter) @@ -28,6 +28,8 @@ class Job: SPUG_SSH_USERNAME=username, SPUG_INTERPRETER=interpreter ) + if isinstance(params, dict): + self.env.update({f'_SPUG_{k}': str(v) for k, v in params.items()}) def _send(self, message, with_expire=False): if self.rds_cli is None: diff --git a/spug_api/apps/exec/models.py b/spug_api/apps/exec/models.py index f3cb77d..d084d52 100644 --- a/spug_api/apps/exec/models.py +++ b/spug_api/apps/exec/models.py @@ -14,7 +14,7 @@ class ExecTemplate(models.Model, ModelMixin): interpreter = models.CharField(max_length=20, default='sh') host_ids = models.TextField(default='[]') desc = models.CharField(max_length=255, null=True) - + parameters = models.TextField(default='[]') created_at = models.CharField(max_length=20, default=human_datetime) created_by = models.ForeignKey(User, models.PROTECT, related_name='+') updated_at = models.CharField(max_length=20, null=True) @@ -26,6 +26,7 @@ class ExecTemplate(models.Model, ModelMixin): def to_view(self): tmp = self.to_dict() tmp['host_ids'] = json.loads(self.host_ids) + tmp['parameters'] = json.loads(self.parameters) return tmp class Meta: @@ -45,8 +46,11 @@ class ExecHistory(models.Model, ModelMixin): def to_view(self): tmp = self.to_dict() tmp['host_ids'] = json.loads(self.host_ids) - if hasattr(self, 'template_name'): - tmp['template_name'] = self.template_name + if self.template: + tmp['template_name'] = self.template.name + tmp['interpreter'] = self.template.interpreter + tmp['parameters'] = json.loads(self.template.parameters) + tmp['command'] = self.template.body return tmp class Meta: diff --git a/spug_api/apps/exec/views.py b/spug_api/apps/exec/views.py index ba16175..06a8cf4 100644 --- a/spug_api/apps/exec/views.py +++ b/spug_api/apps/exec/views.py @@ -31,6 +31,7 @@ class TemplateView(View): Argument('body', help='请输入模版内容'), Argument('interpreter', default='sh'), Argument('host_ids', type=list, handler=json.dumps, default=[]), + Argument('parameters', type=list, handler=json.dumps, default=[]), Argument('desc', required=False) ).parse(request.body) if error is None: @@ -59,7 +60,8 @@ def do_task(request): Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'), Argument('command', help='请输入执行命令内容'), Argument('interpreter', default='sh'), - Argument('template_id', type=int, required=False) + Argument('template_id', type=int, required=False), + Argument('params', type=dict, required=False) ).parse(request.body) if error is None: if not has_host_perm(request.user, form.host_ids): @@ -76,6 +78,7 @@ def do_task(request): username=host.username, command=form.command, pkey=host.private_key, + params=form.params ) rds.rpush(settings.EXEC_WORKER_KEY, json.dumps(data)) form.host_ids.sort() @@ -106,5 +109,5 @@ def do_task(request): @auth('exec.task.do') def get_histories(request): - records = ExecHistory.objects.filter(user=request.user).annotate(template_name=F('template__name')) + records = ExecHistory.objects.filter(user=request.user).select_related('template') return json_response([x.to_view() for x in records]) diff --git a/spug_web/src/pages/exec/task/Output.js b/spug_web/src/pages/exec/task/Output.js index 3ba9233..4729940 100644 --- a/spug_web/src/pages/exec/task/Output.js +++ b/spug_web/src/pages/exec/task/Output.js @@ -23,13 +23,13 @@ let gCurrent; function OutView(props) { const el = useRef() - const [term, setTerm] = useState(new Terminal()) + const [term] = useState(new Terminal()); + const [fitPlugin] = useState(new FitAddon()); const [current, setCurrent] = useState(Object.keys(store.outputs)[0]) useEffect(() => { store.tag = '' gCurrent = current - const fitPlugin = new FitAddon() term.setOption('disableStdin', false) term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei') term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'}) @@ -39,7 +39,6 @@ function OutView(props) { term.write('\x1b[36m### WebSocket connecting ...\x1b[0m') const resize = () => fitPlugin.fit(); window.addEventListener('resize', resize) - setTerm(term) return () => window.removeEventListener('resize', resize); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -55,6 +54,7 @@ function OutView(props) { } term.write(message) socket.send('ok'); + fitPlugin.fit() } socket.onmessage = e => { if (e.data === 'pong') { @@ -145,8 +145,8 @@ function OutView(props) {
{store.outputs[current].title}
openTerminal(current)}/> -
-
+
+
diff --git a/spug_web/src/pages/exec/task/Parameter.js b/spug_web/src/pages/exec/task/Parameter.js new file mode 100644 index 0000000..d355a6e --- /dev/null +++ b/spug_web/src/pages/exec/task/Parameter.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react'; +import { Modal, Form, Input, Select, message } from 'antd'; + + +function Render(props) { + switch (props.type) { + case 'string': + return + case 'password': + return + case 'select': + const options = props.options.split('\n').map(x => x.split(':')) + return ( + + ) + default: + return null + } +} + +export default function Parameter(props) { + const [form] = Form.useForm(); + + function handleSubmit() { + const formData = form.getFieldsValue(); + for (let item of props.parameters.filter(x => x.required)) { + if (!formData[item.variable]) { + return message.error(`${item.name} 是必填项。`) + } + } + props.onOk(formData); + props.onCancel() + } + + return ( + +
+ {props.parameters.map(item => ( + + + + ))} +
+
+ ) +} \ No newline at end of file diff --git a/spug_web/src/pages/exec/task/index.js b/spug_web/src/pages/exec/task/index.js index b49ec96..3ca8b8a 100644 --- a/spug_web/src/pages/exec/task/index.js +++ b/spug_web/src/pages/exec/task/index.js @@ -10,6 +10,7 @@ import { Form, Button, Alert, Radio, Tooltip } from 'antd'; import { ACEditor, AuthDiv, Breadcrumb } from 'components'; import Selector from 'pages/host/Selector'; import TemplateSelector from './TemplateSelector'; +import Parameter from './Parameter'; import Output from './Output'; import { http, cleanCommand } from 'libs'; import moment from 'moment'; @@ -22,6 +23,8 @@ function TaskIndex() { const [command, setCommand] = useState('') const [template_id, setTemplateId] = useState() const [histories, setHistories] = useState([]) + const [parameters, setParameters] = useState([]) + const [visible, setVisible] = useState(false) useEffect(() => { if (!loading) { @@ -30,6 +33,12 @@ function TaskIndex() { } }, [loading]) + useEffect(() => { + if (!command) { + setParameters([]) + } + }, [command]) + useEffect(() => { return () => { store.host_ids = [] @@ -39,9 +48,12 @@ function TaskIndex() { } }, []) - function handleSubmit() { + function handleSubmit(params) { + if (!params && parameters.length > 0) { + return setVisible(true) + } setLoading(true) - const formData = {interpreter, template_id, host_ids: store.host_ids, command: cleanCommand(command)} + const formData = {interpreter, template_id, params, host_ids: store.host_ids, command: cleanCommand(command)} http.post('/api/exec/do/', formData) .then(store.switchConsole) .finally(() => setLoading(false)) @@ -52,12 +64,14 @@ function TaskIndex() { setTemplateId(tpl.id) setInterpreter(tpl.interpreter) setCommand(tpl.body) + setParameters(tpl.parameters) } function handleClick(item) { setTemplateId(item.template_id) setInterpreter(item.interpreter) setCommand(item.command) + setParameters(item.parameters || []) store.host_ids = item.host_ids } @@ -98,7 +112,8 @@ function TaskIndex() { - +
@@ -124,14 +139,15 @@ function TaskIndex() {
- {store.showTemplate && - } + {store.showTemplate && } {store.showConsole && } + {visible && setVisible(false)} onOk={v => handleSubmit(v)}/>} store.showHost = false} onOk={(_, ids) => store.host_ids = ids}/> + ) } diff --git a/spug_web/src/pages/exec/task/index.module.less b/spug_web/src/pages/exec/task/index.module.less index 13c820e..f830497 100644 --- a/spug_web/src/pages/exec/task/index.module.less +++ b/spug_web/src/pages/exec/task/index.module.less @@ -34,6 +34,7 @@ .right { width: 40%; + max-width: 600px; display: flex; flex-direction: column; background-color: #fafafa; @@ -253,11 +254,15 @@ } - .term { - flex: 1; + .termContainer { background-color: #f0f0f0; - padding: 8px 0 8px 12px; + padding: 8px 0 4px 12px; border-radius: 2px; + + .term { + width: 100%; + height: calc(100vh - 300px); + } } } } \ No newline at end of file diff --git a/spug_web/src/pages/exec/template/Form.js b/spug_web/src/pages/exec/template/Form.js index 7f3f819..193fc0e 100644 --- a/spug_web/src/pages/exec/template/Form.js +++ b/spug_web/src/pages/exec/template/Form.js @@ -3,32 +3,41 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; -import { ExclamationCircleOutlined } from '@ant-design/icons'; -import { Modal, Form, Input, Select, Button, Radio, message } from 'antd'; +import { ExclamationCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { Modal, Form, Input, Select, Button, Radio, Table, Tooltip, message } from 'antd'; import { ACEditor } from 'components'; import Selector from 'pages/host/Selector'; +import Parameter from './Parameter'; import { http, cleanCommand } from 'libs'; -import store from './store'; +import lds from 'lodash'; +import S from './store'; export default observer(function () { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); - const [body, setBody] = useState(store.record.body); + const [body, setBody] = useState(S.record.body); + const [parameter, setParameter] = useState(); + const [parameters, setParameters] = useState([]); const [visible, setVisible] = useState(false); + useEffect(() => { + setParameters(S.record.parameters) + }, []) + function handleSubmit() { setLoading(true); const formData = form.getFieldsValue(); - formData['id'] = store.record.id; + formData['id'] = S.record.id; formData['body'] = cleanCommand(body); - formData['host_ids'] = store.record.host_ids; + formData['host_ids'] = S.record.host_ids; + formData['parameters'] = parameters; http.post('/api/exec/template/', formData) .then(res => { message.success('操作成功'); - store.formVisible = false; - store.fetchRecords() + S.formVisible = false; + S.fetchRecords() }, () => setLoading(false)) } @@ -46,28 +55,45 @@ export default observer(function () { ), onOk: () => { if (type) { - store.types.push(type); + S.types.push(type); form.setFieldsValue({type}) } }, }) } - const info = store.record; + function updateParameter(data) { + if (data.id) { + const index = lds.findIndex(parameters, {id: data.id}) + parameters[index] = data + } else { + data.id = parameters.length + 1 + parameters.push(data) + } + setParameters([...parameters]) + setParameter(null) + } + + function delParameter(index) { + parameters.splice(index, 1) + setParameters([...parameters]) + } + + const info = S.record; return ( store.formVisible = false} + title={S.record.id ? '编辑模板' : '新建模板'} + onCancel={() => S.formVisible = false} confirmLoading={loading} onOk={handleSubmit}>
@@ -91,9 +117,24 @@ export default observer(function () { mode={getFieldValue('interpreter')} value={body} onChange={val => setBody(val)} - height="300px"/> + height="250px"/> )} + + {parameters.length > 0 && ( + + {row.name}}/> + + [ +
+ )} + +
{info.host_ids.length > 0 && 已选择 {info.host_ids.length} 台} @@ -107,6 +148,13 @@ export default observer(function () { selectedRowKeys={[...info.host_ids]} onCancel={() => setVisible(false)} onOk={(_, ids) => info.host_ids = ids}/> + {parameter ? ( + setParameter(null)} + onOk={updateParameter}/> + ) : null} ) }) \ No newline at end of file diff --git a/spug_web/src/pages/exec/template/Parameter.js b/spug_web/src/pages/exec/template/Parameter.js new file mode 100644 index 0000000..31818d8 --- /dev/null +++ b/spug_web/src/pages/exec/template/Parameter.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react'; +import { Modal, Form, Input, Radio, Switch, message } from 'antd'; +import S from './store'; +import lds from 'lodash'; + +export default function Parameter(props) { + const [form] = Form.useForm(); + + function handleSubmit() { + const formData = form.getFieldsValue(); + console.log(formData) + formData.id = props.parameter.id + if (!formData.name) return message.error('请输入参数名') + if (!formData.variable) return message.error('请输入变量名') + if (!formData.type) return message.error('请选择参数类型') + if (formData.type === 'select' && !formData.options) return message.error('请输入可选项') + const tmp = lds.find(props.parameters, {variable: formData.variable}) + if (tmp && tmp.id !== formData.id) return message.error('变量名重复') + props.onOk(formData) + } + + return ( + + + + + + + + + + + {Object.entries(S.ParameterTypes).map(([key, val]) => ( + {val} + ))} + + + + {({getFieldValue}) => + ['select'].includes(getFieldValue('type')) ? ( + + + + ) : null + } + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/spug_web/src/pages/exec/template/store.js b/spug_web/src/pages/exec/template/store.js index fe155e0..abb1af5 100644 --- a/spug_web/src/pages/exec/template/store.js +++ b/spug_web/src/pages/exec/template/store.js @@ -7,9 +7,14 @@ import { observable } from "mobx"; import { http, includes } from 'libs'; class Store { + ParameterTypes = { + 'string': '文本框', + 'password': '密码框', + 'select': '下拉选择' + } @observable records = []; @observable types = []; - @observable record = {}; + @observable record = {parameters: []}; @observable isFetching = false; @observable formVisible = false; @@ -33,7 +38,7 @@ class Store { .finally(() => this.isFetching = false) }; - showForm = (info = {interpreter: 'sh', host_ids: []}) => { + showForm = (info = {interpreter: 'sh', host_ids: [], parameters: []}) => { this.formVisible = true; this.record = info }