mirror of https://github.com/openspug/spug
U 批量执行的历史记录,如果来自模版执行,则显示模版名称 #430
parent
01cc1ca8c7
commit
824cf39c43
|
@ -35,6 +35,7 @@ class ExecTemplate(models.Model, ModelMixin):
|
||||||
|
|
||||||
class ExecHistory(models.Model, ModelMixin):
|
class ExecHistory(models.Model, ModelMixin):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
template = models.ForeignKey(ExecTemplate, on_delete=models.SET_NULL, null=True)
|
||||||
digest = models.CharField(max_length=32, db_index=True)
|
digest = models.CharField(max_length=32, db_index=True)
|
||||||
interpreter = models.CharField(max_length=20)
|
interpreter = models.CharField(max_length=20)
|
||||||
command = models.TextField()
|
command = models.TextField()
|
||||||
|
@ -44,6 +45,8 @@ class ExecHistory(models.Model, ModelMixin):
|
||||||
def to_view(self):
|
def to_view(self):
|
||||||
tmp = self.to_dict()
|
tmp = self.to_dict()
|
||||||
tmp['host_ids'] = json.loads(self.host_ids)
|
tmp['host_ids'] = json.loads(self.host_ids)
|
||||||
|
if hasattr(self, 'template_name'):
|
||||||
|
tmp['template_name'] = self.template_name
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_redis import get_redis_connection
|
from django_redis import get_redis_connection
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models import F
|
||||||
from libs import json_response, JsonParser, Argument, human_datetime, auth
|
from libs import json_response, JsonParser, Argument, human_datetime, auth
|
||||||
from apps.exec.models import ExecTemplate, ExecHistory
|
from apps.exec.models import ExecTemplate, ExecHistory
|
||||||
from apps.host.models import Host
|
from apps.host.models import Host
|
||||||
|
@ -57,7 +58,8 @@ def do_task(request):
|
||||||
form, error = JsonParser(
|
form, error = JsonParser(
|
||||||
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'),
|
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'),
|
||||||
Argument('command', help='请输入执行命令内容'),
|
Argument('command', help='请输入执行命令内容'),
|
||||||
Argument('interpreter', default='sh')
|
Argument('interpreter', default='sh'),
|
||||||
|
Argument('template_id', type=int, required=False)
|
||||||
).parse(request.body)
|
).parse(request.body)
|
||||||
if error is None:
|
if error is None:
|
||||||
if not has_host_perm(request.user, form.host_ids):
|
if not has_host_perm(request.user, form.host_ids):
|
||||||
|
@ -81,7 +83,12 @@ def do_task(request):
|
||||||
tmp_str = f'{form.interpreter},{host_ids},{form.command}'
|
tmp_str = f'{form.interpreter},{host_ids},{form.command}'
|
||||||
digest = hashlib.md5(tmp_str.encode()).hexdigest()
|
digest = hashlib.md5(tmp_str.encode()).hexdigest()
|
||||||
record = ExecHistory.objects.filter(user=request.user, digest=digest).first()
|
record = ExecHistory.objects.filter(user=request.user, digest=digest).first()
|
||||||
|
if form.template_id:
|
||||||
|
template = ExecTemplate.objects.filter(pk=form.template_id).first()
|
||||||
|
if not template or template.body != form.command:
|
||||||
|
form.template_id = None
|
||||||
if record:
|
if record:
|
||||||
|
record.template_id = form.template_id
|
||||||
record.updated_at = human_datetime()
|
record.updated_at = human_datetime()
|
||||||
record.save()
|
record.save()
|
||||||
else:
|
else:
|
||||||
|
@ -89,6 +96,7 @@ def do_task(request):
|
||||||
user=request.user,
|
user=request.user,
|
||||||
digest=digest,
|
digest=digest,
|
||||||
interpreter=form.interpreter,
|
interpreter=form.interpreter,
|
||||||
|
template_id=form.template_id,
|
||||||
command=form.command,
|
command=form.command,
|
||||||
host_ids=json.dumps(form.host_ids),
|
host_ids=json.dumps(form.host_ids),
|
||||||
)
|
)
|
||||||
|
@ -98,5 +106,5 @@ def do_task(request):
|
||||||
|
|
||||||
@auth('exec.task.do')
|
@auth('exec.task.do')
|
||||||
def get_histories(request):
|
def get_histories(request):
|
||||||
records = ExecHistory.objects.filter(user=request.user)
|
records = ExecHistory.objects.filter(user=request.user).annotate(template_name=F('template__name'))
|
||||||
return json_response([x.to_view() for x in records])
|
return json_response([x.to_view() for x in records])
|
||||||
|
|
|
@ -32,8 +32,8 @@ class TemplateSelector extends React.Component {
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
if (this.state.selectedRows.length > 0) {
|
if (this.state.selectedRows.length > 0) {
|
||||||
const {host_ids, body, interpreter} = this.state.selectedRows[0]
|
const tpl = this.state.selectedRows[0]
|
||||||
this.props.onOk(host_ids, body, interpreter)
|
this.props.onOk(tpl)
|
||||||
}
|
}
|
||||||
this.props.onCancel()
|
this.props.onCancel()
|
||||||
};
|
};
|
||||||
|
@ -62,13 +62,6 @@ class TemplateSelector extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {selectedRows} = this.state;
|
const {selectedRows} = this.state;
|
||||||
let data = store.records;
|
|
||||||
if (store.f_name) {
|
|
||||||
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
|
|
||||||
}
|
|
||||||
if (store.f_type) {
|
|
||||||
data = data.filter(item => item['type'].toLowerCase().includes(store.f_type.toLowerCase()))
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
|
@ -99,7 +92,7 @@ class TemplateSelector extends React.Component {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
onChange: (_, selectedRows) => this.setState({selectedRows})
|
onChange: (_, selectedRows) => this.setState({selectedRows})
|
||||||
}}
|
}}
|
||||||
dataSource={data}
|
dataSource={store.dataSource}
|
||||||
loading={store.isFetching}
|
loading={store.isFetching}
|
||||||
onRow={record => {
|
onRow={record => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -20,6 +20,7 @@ function TaskIndex() {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [interpreter, setInterpreter] = useState('sh')
|
const [interpreter, setInterpreter] = useState('sh')
|
||||||
const [command, setCommand] = useState('')
|
const [command, setCommand] = useState('')
|
||||||
|
const [template_id, setTemplateId] = useState()
|
||||||
const [histories, setHistories] = useState([])
|
const [histories, setHistories] = useState([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -40,19 +41,21 @@ function TaskIndex() {
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const host_ids = store.host_ids;
|
const formData = {interpreter, template_id, host_ids: store.host_ids, command: cleanCommand(command)}
|
||||||
http.post('/api/exec/do/', {host_ids, interpreter, command: cleanCommand(command)})
|
http.post('/api/exec/do/', formData)
|
||||||
.then(store.switchConsole)
|
.then(store.switchConsole)
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTemplate(host_ids, command, interpreter) {
|
function handleTemplate(tpl) {
|
||||||
if (host_ids.length > 0) store.host_ids = host_ids
|
if (tpl.host_ids.length > 0) store.host_ids = tpl.host_ids
|
||||||
setInterpreter(interpreter)
|
setTemplateId(tpl.id)
|
||||||
setCommand(command)
|
setInterpreter(tpl.interpreter)
|
||||||
|
setCommand(tpl.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(item) {
|
function handleClick(item) {
|
||||||
|
setTemplateId(item.template_id)
|
||||||
setInterpreter(item.interpreter)
|
setInterpreter(item.interpreter)
|
||||||
setCommand(item.command)
|
setCommand(item.command)
|
||||||
store.host_ids = item.host_ids
|
store.host_ids = item.host_ids
|
||||||
|
@ -110,7 +113,11 @@ function TaskIndex() {
|
||||||
<div key={index} className={style.item} onClick={() => handleClick(item)}>
|
<div key={index} className={style.item} onClick={() => handleClick(item)}>
|
||||||
<div className={style[item.interpreter]}>{item.interpreter.substr(0, 2)}</div>
|
<div className={style[item.interpreter]}>{item.interpreter.substr(0, 2)}</div>
|
||||||
<div className={style.number}>{item.host_ids.length}</div>
|
<div className={style.number}>{item.host_ids.length}</div>
|
||||||
|
{item.template_name ? (
|
||||||
|
<div className={style.tpl}>{item.template_name}</div>
|
||||||
|
) : (
|
||||||
<div className={style.command}>{item.command}</div>
|
<div className={style.command}>{item.command}</div>
|
||||||
|
)}
|
||||||
<div className={style.desc}>{moment(item.updated_at).format('MM.DD HH:mm')}</div>
|
<div className={style.desc}>{moment(item.updated_at).format('MM.DD HH:mm')}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
.index {
|
.index {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - 218px);
|
height: calc(100vh - 218px);
|
||||||
|
min-height: 420px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
height: calc(100vh - 482px) !important;
|
height: calc(100vh - 482px) !important;
|
||||||
|
min-height: 152px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +97,17 @@
|
||||||
margin: 0 12px;
|
margin: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tpl {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0 12px;
|
||||||
|
background-color: #d2e7fd;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,20 +32,13 @@ class ComTable extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let data = store.records;
|
|
||||||
if (store.f_name) {
|
|
||||||
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
|
|
||||||
}
|
|
||||||
if (store.f_type) {
|
|
||||||
data = data.filter(item => item['type'].toLowerCase().includes(store.f_type.toLowerCase()))
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<TableCard
|
<TableCard
|
||||||
tKey="et"
|
tKey="et"
|
||||||
title="模板列表"
|
title="模板列表"
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={store.isFetching}
|
loading={store.isFetching}
|
||||||
dataSource={data}
|
dataSource={store.dataSource}
|
||||||
onReload={store.fetchRecords}
|
onReload={store.fetchRecords}
|
||||||
actions={[
|
actions={[
|
||||||
<AuthButton
|
<AuthButton
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* Released under the AGPL-3.0 License.
|
* Released under the AGPL-3.0 License.
|
||||||
*/
|
*/
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import http from 'libs/http';
|
import { http, includes } from 'libs';
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
@observable records = [];
|
@observable records = [];
|
||||||
|
@ -16,6 +16,13 @@ class Store {
|
||||||
@observable f_name;
|
@observable f_name;
|
||||||
@observable f_type;
|
@observable f_type;
|
||||||
|
|
||||||
|
get dataSource() {
|
||||||
|
let data = this.records
|
||||||
|
if (this.f_name) data = data.filter(x => includes(x.name, this.f_name))
|
||||||
|
if (this.f_type) data = data.filter(x => includes(x.type, this.f_type))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
fetchRecords = () => {
|
fetchRecords = () => {
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
http.get('/api/exec/template/')
|
http.get('/api/exec/template/')
|
||||||
|
|
|
@ -167,7 +167,7 @@ export default observer(function () {
|
||||||
<Button disabled={!canNext()} type="link" loading={loading} onClick={handleTest}>执行测试</Button>
|
<Button disabled={!canNext()} type="link" loading={loading} onClick={handleTest}>执行测试</Button>
|
||||||
<span style={{color: '#888', fontSize: 12}}>Tips: 仅测试第一个监控地址</span>
|
<span style={{color: '#888', fontSize: 12}}>Tips: 仅测试第一个监控地址</span>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{showTmp && <TemplateSelector onOk={(_, v) => store.record.extra = v} onCancel={() => setShowTmp(false)}/>}
|
{showTmp && <TemplateSelector onOk={({body}) => store.record.extra = body} onCancel={() => setShowTmp(false)}/>}
|
||||||
<Selector
|
<Selector
|
||||||
visible={showSelector}
|
visible={showSelector}
|
||||||
selectedRowKeys={[...store.record.targets]}
|
selectedRowKeys={[...store.record.targets]}
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default observer(function () {
|
||||||
<Form.Item shouldUpdate wrapperCol={{span: 14, offset: 6}}>
|
<Form.Item shouldUpdate wrapperCol={{span: 14, offset: 6}}>
|
||||||
{() => <Button disabled={canNext()} type="primary" onClick={handleNext}>下一步</Button>}
|
{() => <Button disabled={canNext()} type="primary" onClick={handleNext}>下一步</Button>}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{showTmp && <TemplateSelector onOk={(_, v) => setCommand(v)} onCancel={() => setShowTmp(false)}/>}
|
{showTmp && <TemplateSelector onOk={({body}) => setCommand(body)} onCancel={() => setShowTmp(false)}/>}
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
})
|
})
|
Loading…
Reference in New Issue