diff --git a/spug_api/apps/exec/executors.py b/spug_api/apps/exec/executors.py index cd01e16..c57fcf1 100644 --- a/spug_api/apps/exec/executors.py +++ b/spug_api/apps/exec/executors.py @@ -14,10 +14,10 @@ def exec_worker_handler(job): class Job: - def __init__(self, key, name, hostname, port, username, pkey, command, token=None): + def __init__(self, key, name, hostname, port, username, pkey, command, interpreter, token=None): self.ssh = SSH(hostname, port, username, pkey) self.key = key - self.command = command + self.command = self._handle_command(command, interpreter) self.token = token self.rds_cli = None self.env = dict( @@ -35,6 +35,11 @@ class Job: if with_expire: self.rds_cli.expire(self.token, 300) + def _handle_command(self, command, interpreter): + if interpreter == 'python': + return f'python << EOF\n{command}\nEOF' + return command + def send(self, data): message = {'key': self.key, 'data': data} self._send(message) diff --git a/spug_api/apps/exec/models.py b/spug_api/apps/exec/models.py index 0a98d48..01f4514 100644 --- a/spug_api/apps/exec/models.py +++ b/spug_api/apps/exec/models.py @@ -4,12 +4,14 @@ from django.db import models from libs import ModelMixin, human_datetime from apps.account.models import User +import json class ExecTemplate(models.Model, ModelMixin): name = models.CharField(max_length=50) type = models.CharField(max_length=50) body = models.TextField() + interpreter = models.CharField(max_length=20) desc = models.CharField(max_length=255, null=True) created_at = models.CharField(max_length=20, default=human_datetime) @@ -23,3 +25,20 @@ class ExecTemplate(models.Model, ModelMixin): class Meta: db_table = 'exec_templates' ordering = ('-id',) + + +class ExecHistory(models.Model, ModelMixin): + digest = models.CharField(max_length=32, unique=True) + interpreter = models.CharField(max_length=20) + command = models.TextField() + host_ids = models.TextField() + updated_at = models.CharField(max_length=20, default=human_datetime) + + def to_view(self): + tmp = self.to_dict() + tmp['host_ids'] = json.loads(self.host_ids) + return tmp + + class Meta: + db_table = 'exec_histories' + ordering = ('-id',) diff --git a/spug_api/apps/exec/urls.py b/spug_api/apps/exec/urls.py index 0cdb88a..01aeb60 100644 --- a/spug_api/apps/exec/urls.py +++ b/spug_api/apps/exec/urls.py @@ -7,5 +7,6 @@ from .views import * urlpatterns = [ url(r'template/$', TemplateView.as_view()), + url(r'history/$', get_histories), url(r'do/$', do_task), ] diff --git a/spug_api/apps/exec/views.py b/spug_api/apps/exec/views.py index fa3e5d5..8380e40 100644 --- a/spug_api/apps/exec/views.py +++ b/spug_api/apps/exec/views.py @@ -5,9 +5,10 @@ from django.views.generic import View from django_redis import get_redis_connection from django.conf import settings from libs import json_response, JsonParser, Argument, human_datetime, auth -from apps.exec.models import ExecTemplate +from apps.exec.models import ExecTemplate, ExecHistory from apps.host.models import Host from apps.account.utils import has_host_perm +import hashlib import uuid import json @@ -26,6 +27,7 @@ class TemplateView(View): Argument('name', help='请输入模版名称'), Argument('type', help='请选择模版类型'), Argument('body', help='请输入模版内容'), + Argument('interpreter', default='sh'), Argument('desc', required=False) ).parse(request.body) if error is None: @@ -52,7 +54,8 @@ class TemplateView(View): def do_task(request): form, error = JsonParser( Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'), - Argument('command', help='请输入执行命令内容') + Argument('command', help='请输入执行命令内容'), + Argument('interpreter', default='sh') ).parse(request.body) if error is None: if not has_host_perm(request.user, form.host_ids): @@ -63,6 +66,7 @@ def do_task(request): key=host.id, name=host.name, token=token, + interpreter=form.interpreter, hostname=host.hostname, port=host.port, username=host.username, @@ -70,5 +74,26 @@ def do_task(request): pkey=host.private_key, ) rds.rpush(settings.EXEC_WORKER_KEY, json.dumps(data)) + form.host_ids.sort() + host_ids = json.dumps(form.host_ids) + tmp_str = f'{form.interpreter},{host_ids},{form.command}' + digest = hashlib.md5(tmp_str.encode()).hexdigest() + record = ExecHistory.objects.filter(digest=digest).first() + if record: + record.updated_at = human_datetime() + record.save() + else: + ExecHistory.objects.create( + digest=digest, + interpreter=form.interpreter, + command=form.command, + host_ids=json.dumps(form.host_ids), + ) return json_response(token) return json_response(error=error) + + +@auth('exec.task.do') +def get_histories(request): + records = ExecHistory.objects.all() + return json_response([x.to_view() for x in records]) diff --git a/spug_web/src/pages/exec/task/TemplateSelector.js b/spug_web/src/pages/exec/task/TemplateSelector.js index ee3c66a..5ed991a 100644 --- a/spug_web/src/pages/exec/task/TemplateSelector.js +++ b/spug_web/src/pages/exec/task/TemplateSelector.js @@ -32,7 +32,8 @@ class TemplateSelector extends React.Component { handleSubmit = () => { if (this.state.selectedRows.length > 0) { - this.props.onOk(this.state.selectedRows[0].body) + const {body, interpreter} = this.state.selectedRows[0] + this.props.onOk(body, interpreter) } this.props.onCancel() }; diff --git a/spug_web/src/pages/exec/task/index.js b/spug_web/src/pages/exec/task/index.js index 0f7c0f4..fb49fa1 100644 --- a/spug_web/src/pages/exec/task/index.js +++ b/spug_web/src/pages/exec/task/index.js @@ -3,79 +3,112 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; -import { PlusOutlined, ThunderboltOutlined } from '@ant-design/icons'; -import { Form, Button, Card, Alert } from 'antd'; +import { PlusOutlined, ThunderboltOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { Form, Button, Card, Alert, Radio, Tooltip } from 'antd'; import { ACEditor, AuthDiv, Breadcrumb } from 'components'; import Selector from 'pages/host/Selector'; import TemplateSelector from './TemplateSelector'; import ExecConsole from './ExecConsole'; import { http, cleanCommand } from 'libs'; +import moment from 'moment'; import store from './store'; +import style from './index.module.less'; -@observer -class TaskIndex extends React.Component { - constructor(props) { - super(props); - this.state = { - loading: false, - body: '', - } - } +function TaskIndex() { + const [loading, setLoading] = useState(false) + const [interpreter, setInterpreter] = useState('sh') + const [command, setCommand] = useState('') + const [histories, setHistories] = useState([]) - componentWillUnmount() { - store.host_ids = [] - } + useEffect(() => { + http.get('/api/exec/history/') + .then(res => setHistories(res)) + }, []) - handleSubmit = () => { - this.setState({loading: true}); + function handleSubmit() { + setLoading(true) const host_ids = store.host_ids; - http.post('/api/exec/do/', {host_ids, command: cleanCommand(this.state.body)}) + http.post('/api/exec/do/', {host_ids, interpreter, command: cleanCommand(command)}) .then(store.switchConsole) - .finally(() => this.setState({loading: false})) - }; - - render() { - const {body, token} = this.state; - return ( - - - 首页 - 批量执行 - 执行任务 - - -
- - {store.host_ids.length > 0 && ( - - )} - - - - this.setState({body})}/> - - - - - -
-
- {store.showTemplate && - this.setState({body: body + v})}/>} - {store.showConsole && } - store.showHost = false} - onOk={(_, ids) => store.host_ids = ids}/> -
- ); + .finally(() => setLoading(false)) } + + function handleTemplate(command, interpreter) { + setInterpreter(interpreter) + setCommand(command) + } + + function handleClick(item) { + setInterpreter(item.interpreter) + setCommand(item.command) + store.host_ids = item.host_ids + } + + return ( + + + 首页 + 批量执行 + 执行任务 + + +
+ + {store.host_ids.length > 0 && ( + + )} + + + + setInterpreter(e.target.value)}> + Shell + Python + + + + + + + +
+
+
+ 执行记录 + + + +
+
+ {histories.map((item, index) => ( +
handleClick(item)}> +
{item.interpreter.substr(0, 2)}
+
{item.host_ids.length}
+
{item.command}
+
{moment(item.updated_at).format('MM.DD HH:mm')}
+
+ ))} +
+
+
+ {store.showTemplate && + } + {store.showConsole && } + store.showHost = false} + onOk={(_, ids) => store.host_ids = ids}/> +
+ ) } -export default TaskIndex +export default observer(TaskIndex) diff --git a/spug_web/src/pages/exec/task/index.module.less b/spug_web/src/pages/exec/task/index.module.less index 1030233..4f0e1ad 100644 --- a/spug_web/src/pages/exec/task/index.module.less +++ b/spug_web/src/pages/exec/task/index.module.less @@ -41,4 +41,81 @@ pre { margin: 0; +} + +.hisBlock { + width: 40%; + display: flex; + flex-direction: column; + background-color: #fafafa; + padding: 24px; + overflow: auto; + + .title { + font-weight: 500; + margin-bottom: 12px; + } + + .inner { + max-height: 700px; + overflow: auto; + } + + .item { + display: flex; + align-items: center; + border-radius: 2px; + padding: 8px 12px; + cursor: pointer; + margin-bottom: 12px; + + .sh { + width: 20px; + height: 20px; + line-height: 20px; + text-align: center; + color: #fff; + font-weight: 500; + border-radius: 2px; + background-color: #1890ff; + } + + .python { + display: flex; + justify-content: center; + align-items: center; + color: #fff; + width: 20px; + height: 20px; + font-weight: 500; + border-radius: 2px; + background-color: #dca900; + } + + .number { + width: 24px; + text-align: center; + margin-left: 12px; + border-radius: 2px; + font-weight: 500; + background-color:#dfdfdf; + } + + .command { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0 12px; + } + + .desc { + color: #999; + } + } + + .item:hover { + border-color: #1890ff; + background-color: #e6f7ff; + } } \ 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 212fcf9..5ceffbe 100644 --- a/spug_web/src/pages/exec/template/Form.js +++ b/spug_web/src/pages/exec/template/Form.js @@ -6,7 +6,7 @@ import React, { useState } from 'react'; import { observer } from 'mobx-react'; import { ExclamationCircleOutlined } from '@ant-design/icons'; -import { Modal, Form, Input, Select, Button, message } from 'antd'; +import { Modal, Form, Input, Select, Button, Radio, message } from 'antd'; import { ACEditor } from 'components'; import { http, cleanCommand } from 'libs'; import store from './store'; @@ -76,12 +76,20 @@ export default observer(function () { - - setBody(val)} - height="300px"/> + + + Shell + Python + + + p.interpreter !== c.interpreter}> + {({getFieldValue}) => ( + setBody(val)} + height="300px"/> + )} diff --git a/spug_web/src/pages/exec/template/store.js b/spug_web/src/pages/exec/template/store.js index 16cfa31..541f10e 100644 --- a/spug_web/src/pages/exec/template/store.js +++ b/spug_web/src/pages/exec/template/store.js @@ -26,7 +26,7 @@ class Store { .finally(() => this.isFetching = false) }; - showForm = (info = {}) => { + showForm = (info = {interpreter: 'sh'}) => { this.formVisible = true; this.record = info }