mirror of https://github.com/openspug/spug
A 新增任务计划触发器"监控告警",可用于故障自愈
parent
7e6cba42a0
commit
cc4829772e
|
@ -3,7 +3,7 @@
|
|||
# Released under the AGPL-3.0 License.
|
||||
from django_redis import get_redis_connection
|
||||
from apps.host.models import Host
|
||||
from apps.monitor.utils import handle_notify
|
||||
from apps.monitor.utils import handle_notify, handle_trigger_event
|
||||
from socket import socket
|
||||
import subprocess
|
||||
import platform
|
||||
|
@ -105,6 +105,7 @@ def monitor_worker_handler(job):
|
|||
if not v_time or int(time.time()) - int(v_time) >= quiet * 60:
|
||||
rds.hset(key, f_time, int(time.time()))
|
||||
logging.warning('send fault alarm notification')
|
||||
handle_trigger_event(task_id, addr if tp in ('3', '4') else None)
|
||||
handle_notify(task_id, target, is_ok, message, v_count)
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
from django.db import close_old_connections
|
||||
from apps.alarm.models import Alarm
|
||||
from apps.monitor.models import Detection
|
||||
from apps.schedule.models import Task
|
||||
from apps.schedule.scheduler import Scheduler
|
||||
from libs.spug import Notification
|
||||
import json
|
||||
|
||||
|
@ -41,3 +43,17 @@ def handle_notify(task_id, target, is_ok, out, fault_times):
|
|||
grp = json.loads(det.notify_grp)
|
||||
notify = Notification(grp, event, target, det.name, out, duration)
|
||||
notify.dispatch_monitor(json.loads(det.notify_mode))
|
||||
|
||||
|
||||
def handle_trigger_event(task_id, target):
|
||||
query = dict(is_active=True, trigger='monitor', trigger_args__regex=fr'[^0-9]{task_id}[^0-9]')
|
||||
for item in Task.objects.filter(**query):
|
||||
targets = []
|
||||
for t in json.loads(item.targets):
|
||||
if t == 'monitor':
|
||||
if target:
|
||||
targets.append(target)
|
||||
else:
|
||||
targets.append(t)
|
||||
if targets:
|
||||
Scheduler.dispatch(item.id, item.interpreter, item.command, targets)
|
||||
|
|
|
@ -31,9 +31,9 @@ class History(models.Model, ModelMixin):
|
|||
class Task(models.Model, ModelMixin):
|
||||
TRIGGERS = (
|
||||
('date', '一次性'),
|
||||
('calendarinterval', '日历间隔'),
|
||||
('cron', 'UNIX cron'),
|
||||
('interval', '普通间隔')
|
||||
('interval', '普通间隔'),
|
||||
('monitor', '监控告警'),
|
||||
)
|
||||
name = models.CharField(max_length=50)
|
||||
type = models.CharField(max_length=50)
|
||||
|
@ -54,12 +54,13 @@ class Task(models.Model, ModelMixin):
|
|||
|
||||
def to_dict(self, *args, **kwargs):
|
||||
tmp = super().to_dict(*args, **kwargs)
|
||||
tmp['trigger_alias'] = self.get_trigger_display()
|
||||
tmp['targets'] = json.loads(self.targets)
|
||||
tmp['latest_status'] = self.latest.status if self.latest else None
|
||||
tmp['latest_run_time'] = self.latest.run_time if self.latest else None
|
||||
tmp['latest_status_alias'] = self.latest.get_status_display() if self.latest else None
|
||||
tmp['rst_notify'] = json.loads(self.rst_notify) if self.rst_notify else {'mode': '0'}
|
||||
if self.trigger == 'cron':
|
||||
if self.trigger in ('cron', 'monitor'):
|
||||
tmp['trigger_args'] = json.loads(self.trigger_args)
|
||||
return tmp
|
||||
|
||||
|
|
|
@ -58,11 +58,8 @@ class Scheduler:
|
|||
else:
|
||||
raise TypeError(f'unknown schedule policy: {trigger!r}')
|
||||
|
||||
def _init_builtin_jobs(self):
|
||||
self.scheduler.add_job(auto_run_by_day, 'cron', hour=1, minute=20)
|
||||
self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=1)
|
||||
|
||||
def _dispatch(self, task_id, interpreter, command, targets):
|
||||
@classmethod
|
||||
def dispatch(cls, task_id, interpreter, command, targets):
|
||||
output = {x: None for x in targets}
|
||||
history = History.objects.create(
|
||||
task_id=task_id,
|
||||
|
@ -76,14 +73,18 @@ class Scheduler:
|
|||
rds_cli.rpush(SCHEDULE_WORKER_KEY, json.dumps([history.id, t, interpreter, command]))
|
||||
connections.close_all()
|
||||
|
||||
def _init_builtin_jobs(self):
|
||||
self.scheduler.add_job(auto_run_by_day, 'cron', hour=1, minute=20)
|
||||
self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=1)
|
||||
|
||||
def _init(self):
|
||||
self.scheduler.start()
|
||||
self._init_builtin_jobs()
|
||||
try:
|
||||
for task in Task.objects.filter(is_active=True):
|
||||
for task in Task.objects.filter(is_active=True).exclude(trigger='monitor'):
|
||||
trigger = self.parse_trigger(task.trigger, task.trigger_args)
|
||||
self.scheduler.add_job(
|
||||
self._dispatch,
|
||||
self.dispatch,
|
||||
trigger,
|
||||
id=str(task.id),
|
||||
args=(task.id, task.interpreter, task.command, json.loads(task.targets)),
|
||||
|
@ -103,7 +104,7 @@ class Scheduler:
|
|||
if task.action in ('add', 'modify'):
|
||||
trigger = self.parse_trigger(task.trigger, task.trigger_args)
|
||||
self.scheduler.add_job(
|
||||
self._dispatch,
|
||||
self.dispatch,
|
||||
trigger,
|
||||
id=str(task.id),
|
||||
args=(task.id, task.interpreter, task.command, task.targets),
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
|
||||
function Container(props) {
|
||||
const {visible, style, className} = props;
|
||||
|
||||
return (
|
||||
<div style={{display: visible ? 'block' : 'none', ...style}} className={className}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Container.defaultProps = {
|
||||
visible: true
|
||||
}
|
||||
|
||||
export default Container
|
|
@ -16,6 +16,7 @@ import TableCard from './TableCard';
|
|||
import Breadcrumb from './Breadcrumb';
|
||||
import AppSelector from './AppSelector';
|
||||
import NotFound from './NotFound';
|
||||
import Container from './Container';
|
||||
|
||||
export {
|
||||
StatisticsCard,
|
||||
|
@ -31,4 +32,5 @@ export {
|
|||
Breadcrumb,
|
||||
AppSelector,
|
||||
NotFound,
|
||||
Container,
|
||||
}
|
||||
|
|
|
@ -42,9 +42,22 @@ class Store {
|
|||
return records
|
||||
}
|
||||
|
||||
@computed get cascaderOptions() {
|
||||
let data = {}
|
||||
for (let i of this.records) {
|
||||
const tmp = {value: i.id, label: i.name}
|
||||
if (data[i.group]) {
|
||||
data[i.group]['children'].push(tmp)
|
||||
} else {
|
||||
data[i.group] = {value: i.group, label: i.group, children: [tmp]}
|
||||
}
|
||||
}
|
||||
return Object.values(data)
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.isFetching = true;
|
||||
http.get('/api/monitor/')
|
||||
return http.get('/api/monitor/')
|
||||
.then(({groups, detections}) => {
|
||||
const tmp = new Set();
|
||||
detections.map(item => {
|
||||
|
|
|
@ -10,7 +10,7 @@ import Step1 from './Step1';
|
|||
import Step2 from './Step2';
|
||||
import Step3 from './Step3';
|
||||
import store from './store';
|
||||
import styles from './index.module.css';
|
||||
import styles from './index.module.less';
|
||||
import hostStore from '../host/store';
|
||||
|
||||
export default observer(function () {
|
||||
|
@ -28,12 +28,12 @@ export default observer(function () {
|
|||
footer={null}>
|
||||
<Steps current={store.page} className={styles.steps}>
|
||||
<Steps.Step key={0} title="创建任务"/>
|
||||
<Steps.Step key={1} title="选择执行对象"/>
|
||||
<Steps.Step key={2} title="设置触发器"/>
|
||||
<Steps.Step key={1} title="设置触发器"/>
|
||||
<Steps.Step key={2} title="选择执行对象"/>
|
||||
</Steps>
|
||||
{store.page === 0 && <Step1/>}
|
||||
{store.page === 1 && <Step2/>}
|
||||
{store.page === 2 && <Step3/>}
|
||||
<Step1 visible={store.page === 0}/>
|
||||
<Step2 visible={store.page === 1}/>
|
||||
<Step3 visible={store.page === 2}/>
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Form, Input, Select, Modal, Button, Radio } from 'antd';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { LinkButton, ACEditor } from 'components';
|
||||
import { LinkButton, ACEditor, Container } from 'components';
|
||||
import TemplateSelector from '../exec/task/TemplateSelector';
|
||||
import { cleanCommand } from 'libs';
|
||||
import store from './store';
|
||||
|
||||
export default observer(function () {
|
||||
export default observer(function (props) {
|
||||
const [form] = Form.useForm();
|
||||
const [showTmp, setShowTmp] = useState(false);
|
||||
const [command, setCommand] = useState(store.record.command || '');
|
||||
|
@ -68,65 +73,68 @@ export default observer(function () {
|
|||
}
|
||||
|
||||
return (
|
||||
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required label="任务类型" style={{marginBottom: 0}}>
|
||||
<Form.Item name="type" style={{display: 'inline-block', width: '80%'}}>
|
||||
<Select placeholder="请选择任务类型">
|
||||
{store.types.map(item => (
|
||||
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Container visible={props.visible}>
|
||||
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
|
||||
<Form.Item required label="任务类型" style={{marginBottom: 0}}>
|
||||
<Form.Item name="type" style={{display: 'inline-block', width: '80%'}}>
|
||||
<Select placeholder="请选择任务类型">
|
||||
{store.types.map(item => (
|
||||
<Select.Option value={item} key={item}>{item}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item style={{display: 'inline-block', width: '20%', textAlign: 'right'}}>
|
||||
<Button type="link" onClick={handleAddZone}>添加类型</Button>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item style={{display: 'inline-block', width: '20%', textAlign: 'right'}}>
|
||||
<Button type="link" onClick={handleAddZone}>添加类型</Button>
|
||||
<Form.Item required name="name" label="任务名称">
|
||||
<Input placeholder="请输入任务名称"/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item required name="name" label="任务名称">
|
||||
<Input placeholder="请输入任务名称"/>
|
||||
</Form.Item>
|
||||
<Form.Item required label="任务内容" extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}>
|
||||
<Form.Item noStyle name="interpreter">
|
||||
<Radio.Group buttonStyle="solid" style={{marginBottom: 12}}>
|
||||
<Radio.Button value="sh" style={{width: 80, textAlign: 'center'}}>Shell</Radio.Button>
|
||||
<Radio.Button value="python" style={{width: 80, textAlign: 'center'}}>Python</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Form.Item required label="任务内容"
|
||||
extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}>
|
||||
<Form.Item noStyle name="interpreter">
|
||||
<Radio.Group buttonStyle="solid" style={{marginBottom: 12}}>
|
||||
<Radio.Button value="sh" style={{width: 80, textAlign: 'center'}}>Shell</Radio.Button>
|
||||
<Radio.Button value="python" style={{width: 80, textAlign: 'center'}}>Python</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({getFieldValue}) => (
|
||||
<ACEditor mode={getFieldValue('interpreter')} value={command} width="100%" height="150px"
|
||||
onChange={setCommand}/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({getFieldValue}) => (
|
||||
<ACEditor mode={getFieldValue('interpreter')} value={command} width="100%" height="150px"
|
||||
onChange={setCommand}/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Form.Item label="失败通知" extra={(
|
||||
<span>
|
||||
<Form.Item label="失败通知" extra={(
|
||||
<span>
|
||||
任务执行失败告警通知,
|
||||
<a target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.cc/docs/use-problem#use-dd">钉钉收不到通知?</a>
|
||||
</span>)}>
|
||||
<Input
|
||||
value={store.record.rst_notify.value}
|
||||
onChange={e => store.record.rst_notify.value = e.target.value}
|
||||
addonBefore={(
|
||||
<Select style={{width: 100}} value={store.record.rst_notify.mode}
|
||||
onChange={v => store.record.rst_notify.mode = v}>
|
||||
<Select.Option value="0">关闭</Select.Option>
|
||||
<Select.Option value="1">钉钉</Select.Option>
|
||||
<Select.Option value="4">飞书</Select.Option>
|
||||
<Select.Option value="3">企业微信</Select.Option>
|
||||
<Select.Option value="2">Webhook</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
disabled={store.record.rst_notify.mode === '0'}
|
||||
placeholder={modePlaceholder}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label="备注信息">
|
||||
<Input.TextArea placeholder="请输入模板备注信息"/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate wrapperCol={{span: 14, offset: 6}}>
|
||||
{() => <Button disabled={canNext()} type="primary" onClick={handleNext}>下一步</Button>}
|
||||
</Form.Item>
|
||||
{showTmp && <TemplateSelector onOk={handleSelect} onCancel={() => setShowTmp(false)}/>}
|
||||
</Form>
|
||||
<Input
|
||||
value={store.record.rst_notify.value}
|
||||
onChange={e => store.record.rst_notify.value = e.target.value}
|
||||
addonBefore={(
|
||||
<Select style={{width: 100}} value={store.record.rst_notify.mode}
|
||||
onChange={v => store.record.rst_notify.mode = v}>
|
||||
<Select.Option value="0">关闭</Select.Option>
|
||||
<Select.Option value="1">钉钉</Select.Option>
|
||||
<Select.Option value="4">飞书</Select.Option>
|
||||
<Select.Option value="3">企业微信</Select.Option>
|
||||
<Select.Option value="2">Webhook</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
disabled={store.record.rst_notify.mode === '0'}
|
||||
placeholder={modePlaceholder}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="desc" label="备注信息">
|
||||
<Input.TextArea placeholder="请输入模板备注信息"/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate wrapperCol={{span: 14, offset: 6}}>
|
||||
{() => <Button disabled={canNext()} type="primary" onClick={handleNext}>下一步</Button>}
|
||||
</Form.Item>
|
||||
{showTmp && <TemplateSelector onOk={handleSelect} onCancel={() => setShowTmp(false)}/>}
|
||||
</Form>
|
||||
</Container>
|
||||
)
|
||||
})
|
|
@ -3,61 +3,182 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Select, Button } from 'antd';
|
||||
import HostSelector from 'pages/host/Selector';
|
||||
import store from './store';
|
||||
import hostStore from 'pages/host/store';
|
||||
import styles from './index.module.css';
|
||||
import { Form, Tabs, DatePicker, InputNumber, Input, Button, Cascader } from 'antd';
|
||||
import { LoadingOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Container } from 'components';
|
||||
import { http, includes } from 'libs';
|
||||
import S from './store';
|
||||
import moment from 'moment';
|
||||
import lds from 'lodash';
|
||||
import styles from './index.module.less';
|
||||
import mStore from '../monitor/store';
|
||||
|
||||
export default observer(function () {
|
||||
const [visible, setVisible] = useState(false)
|
||||
let lastFetchId = 0;
|
||||
|
||||
export default observer(function (props) {
|
||||
const [nextRunTime, setNextRunTime] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (mStore.records.length === 0) {
|
||||
mStore.fetchRecords()
|
||||
.then(_initial)
|
||||
} else {
|
||||
_initial()
|
||||
}
|
||||
}, [])
|
||||
|
||||
function _initial() {
|
||||
if (S.trigger_args?.monitor) {
|
||||
let args = S.trigger_args.monitor
|
||||
args = args.map(x => lds.find(mStore.records, {id: x}))
|
||||
.filter(x => x).map(x => ([x.group, x.id]))
|
||||
S.trigger_args.monitor = args
|
||||
}
|
||||
}
|
||||
|
||||
function handleArgs(key, val) {
|
||||
S.trigger_args[key] = val
|
||||
}
|
||||
|
||||
function handleCronArgs(key, val) {
|
||||
let tmp = S.trigger_args['cron'] || {};
|
||||
tmp[key] = val
|
||||
S.trigger_args['cron'] = tmp
|
||||
_fetchNextRunTime()
|
||||
}
|
||||
|
||||
function handleMonitorArgs(index, val) {
|
||||
const tmp = S.trigger_args['monitor'] ?? [[]]
|
||||
if (index === undefined) {
|
||||
tmp.push([])
|
||||
} else if (val) {
|
||||
tmp[index] = val
|
||||
} else {
|
||||
tmp.splice(index, 1)
|
||||
}
|
||||
S.trigger_args['monitor'] = tmp
|
||||
}
|
||||
|
||||
function isValid() {
|
||||
switch (S.trigger) {
|
||||
case 'cron':
|
||||
return lds.get(S.trigger_args, 'cron.rule')
|
||||
case 'monitor':
|
||||
return lds.get(S.trigger_args, 'monitor', []).map(x => x[1]).filter(x => x).length
|
||||
default:
|
||||
return S.trigger_args[S.trigger]
|
||||
}
|
||||
}
|
||||
|
||||
function _fetchNextRunTime() {
|
||||
if (S.trigger === 'cron') {
|
||||
const {rule, start, stop} = lds.get(S.trigger_args, 'cron', {});
|
||||
if (rule && rule.trim().split(/ +/).length === 5) {
|
||||
setNextRunTime(<LoadingOutlined/>);
|
||||
lastFetchId += 1;
|
||||
const fetchId = lastFetchId;
|
||||
const formData = {rule}
|
||||
if (start) formData.start = start.format('YYYY-MM-DD HH:mm:ss')
|
||||
if (stop) formData.stop = stop.format('YYYY-MM-DD HH:mm:ss')
|
||||
http.post('/api/schedule/run_time/', formData)
|
||||
.then(res => {
|
||||
if (fetchId !== lastFetchId) return;
|
||||
if (res.success) {
|
||||
setNextRunTime(<span style={{fontSize: 12, color: '#52c41a'}}>{res.msg}</span>)
|
||||
} else {
|
||||
setNextRunTime(<span style={{fontSize: 12, color: '#ff4d4f'}}>{res.msg}</span>)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setNextRunTime(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} style={{minHeight: 350}}>
|
||||
<Form.Item required label="执行对象">
|
||||
{store.targets.map((id, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Select
|
||||
value={id}
|
||||
showSearch
|
||||
placeholder="请选择"
|
||||
optionFilterProp="children"
|
||||
style={{width: '80%', marginRight: 10, marginBottom: 12}}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
onChange={v => store.editTarget(index, v)}>
|
||||
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option>
|
||||
{hostStore.rawRecords.map(item => (
|
||||
<Select.Option key={item.id} value={item.id} disabled={store.targets.includes(item.id)}>
|
||||
{`${item.name}(${item['hostname']}:${item['port']})`}
|
||||
</Select.Option>
|
||||
<Container visible={props.visible}>
|
||||
<Form layout="vertical" wrapperCol={{span: 14, offset: 5}}>
|
||||
<Form.Item>
|
||||
<Tabs activeKey={S.trigger} className={styles.tabs} onChange={v => S.trigger = v} tabPosition="left">
|
||||
<Tabs.TabPane tab="普通间隔" key="interval">
|
||||
<Form.Item required label="间隔时间(秒)" extra="每隔指定n秒执行一次。">
|
||||
<InputNumber
|
||||
min={10}
|
||||
style={{width: 200}}
|
||||
placeholder="请输入"
|
||||
value={S.trigger_args.interval}
|
||||
onChange={v => handleArgs('interval', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="一次性" key="date">
|
||||
<Form.Item required label="执行时间" extra="仅在指定时间运行一次。">
|
||||
<DatePicker
|
||||
showTime
|
||||
disabledDate={v => v < moment()}
|
||||
style={{width: 200}}
|
||||
placeholder="请选择执行时间"
|
||||
onOk={() => false}
|
||||
value={S.trigger_args.date}
|
||||
onChange={v => handleArgs('date', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="UNIX Cron" key="cron">
|
||||
<Form.Item required label="执行规则" extra="兼容Cron风格,可参考官方例子。">
|
||||
<Input
|
||||
suffix={nextRunTime || <span/>}
|
||||
value={S.trigger_args.cron?.rule}
|
||||
placeholder="例如每天凌晨1点执行:0 1 * * *"
|
||||
onChange={e => handleCronArgs('rule', e.target.value)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="生效时间" extra="定义的执行规则在到达该时间后生效。">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={S.trigger_args.cron?.start}
|
||||
onChange={v => handleCronArgs('start', v)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="结束时间" extra="执行规则在到达该时间后不再执行。">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={S.trigger_args.cron?.stop}
|
||||
onChange={v => handleCronArgs('stop', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="监控告警" key="monitor">
|
||||
<Form.Item required label="监控项目">
|
||||
{lds.get(S.trigger_args, 'monitor', [[]]).map((item, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Cascader
|
||||
placeholder="请选择"
|
||||
value={item}
|
||||
options={mStore.cascaderOptions}
|
||||
style={{width: '80%', marginRight: 10, marginBottom: 12}}
|
||||
showSearch={{filter: (i, p) => p.some(o => includes(o.label, i))}}
|
||||
onChange={v => handleMonitorArgs(index, v)}/>
|
||||
{S.trigger_args.monitor?.length > 1 && (
|
||||
<MinusCircleOutlined className={styles.delIcon} onClick={() => handleMonitorArgs(index)}/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Select>
|
||||
{store.targets.length > 1 && (
|
||||
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delTarget(index)}/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form.Item>
|
||||
<Form.Item extra="当监控项触发告警时执行。">
|
||||
<Button type="dashed" style={{width: '80%'}} onClick={() => handleMonitorArgs()}>
|
||||
<PlusOutlined/>添加监控项
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="dashed" style={{width: '80%'}} onClick={() => setVisible(true)}>
|
||||
<PlusOutlined/>添加执行对象
|
||||
</Button>
|
||||
<Button type="primary" disabled={!isValid()} onClick={() => S.page += 1}>下一步</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => S.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
<HostSelector
|
||||
visible={visible}
|
||||
selectedRowKeys={[...store.targets]}
|
||||
onCancel={() => setVisible(false)}
|
||||
onOk={(_, ids) => store.targets = ids}/>
|
||||
</Form>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button disabled={store.targets.filter(x => x).length === 0} type="primary"
|
||||
onClick={() => store.page += 1}>下一步</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</React.Fragment>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,143 +1,103 @@
|
|||
/**
|
||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Form, Tabs, DatePicker, InputNumber, Input, Button, message } from 'antd';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { http } from 'libs';
|
||||
import store from './store';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Select, Button, message } from 'antd';
|
||||
import { Container } from 'components';
|
||||
import HostSelector from 'pages/host/Selector';
|
||||
import hostStore from 'pages/host/store';
|
||||
import moment from 'moment';
|
||||
import lds from 'lodash';
|
||||
import { http } from 'libs';
|
||||
import S from './store';
|
||||
import styles from './index.module.less';
|
||||
|
||||
let lastFetchId = 0;
|
||||
|
||||
export default observer(function () {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [trigger, setTrigger] = useState(store.record.trigger);
|
||||
const [args, setArgs] = useState({[store.record.trigger]: store.record.trigger_args});
|
||||
const [nextRunTime, setNextRunTime] = useState(null);
|
||||
export default observer(function (props) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
function handleSubmit() {
|
||||
if (trigger === 'date' && args['date'] <= moment()) {
|
||||
return message.error('任务执行时间不能早于当前时间')
|
||||
}
|
||||
setLoading(true)
|
||||
const formData = lds.pick(store.record, ['id', 'name', 'type', 'interpreter', 'command', 'desc', 'rst_notify']);
|
||||
formData['targets'] = store.targets.filter(x => x);
|
||||
formData['trigger'] = trigger;
|
||||
const formData = lds.pick(S.record, ['id', 'name', 'type', 'interpreter', 'command', 'desc', 'rst_notify']);
|
||||
formData['targets'] = S.targets.filter(x => x);
|
||||
formData['trigger'] = S.trigger;
|
||||
formData['trigger_args'] = _parse_args();
|
||||
http.post('/api/schedule/', formData)
|
||||
.then(res => {
|
||||
message.success('操作成功');
|
||||
store.formVisible = false;
|
||||
store.fetchRecords()
|
||||
S.formVisible = false;
|
||||
S.fetchRecords()
|
||||
}, () => setLoading(false))
|
||||
}
|
||||
|
||||
function handleArgs(key, val) {
|
||||
setArgs(Object.assign({}, args, {[key]: val}))
|
||||
}
|
||||
|
||||
function handleCronArgs(key, val) {
|
||||
let tmp = args['cron'] || {};
|
||||
tmp = Object.assign(tmp, {[key]: val});
|
||||
setArgs(Object.assign({}, args, {cron: tmp}));
|
||||
_fetchNextRunTime()
|
||||
}
|
||||
|
||||
function _parse_args() {
|
||||
switch (trigger) {
|
||||
switch (S.trigger) {
|
||||
case 'date':
|
||||
return moment(args['date']).format('YYYY-MM-DD HH:mm:ss');
|
||||
return S.trigger_args.date.format('YYYY-MM-DD HH:mm:ss');
|
||||
case 'cron':
|
||||
const {rule, start, stop} = args['cron'];
|
||||
const {rule, start, stop} = S.trigger_args.cron;
|
||||
return JSON.stringify({
|
||||
rule,
|
||||
start: start ? moment(start).format('YYYY-MM-DD HH:mm:ss') : null,
|
||||
stop: stop ? moment(stop).format('YYYY-MM-DD HH:mm:ss') : null
|
||||
});
|
||||
case 'monitor':
|
||||
return JSON.stringify(S.trigger_args.monitor.map(x => x[1]).filter(x => x))
|
||||
default:
|
||||
return args[trigger];
|
||||
}
|
||||
}
|
||||
|
||||
function _fetchNextRunTime() {
|
||||
if (trigger === 'cron') {
|
||||
const rule = lds.get(args, 'cron.rule');
|
||||
if (rule && rule.trim().split(/ +/).length === 5) {
|
||||
setNextRunTime(<LoadingOutlined/>);
|
||||
lastFetchId += 1;
|
||||
const fetchId = lastFetchId;
|
||||
const args = _parse_args();
|
||||
http.post('/api/schedule/run_time/', JSON.parse(args))
|
||||
.then(res => {
|
||||
if (fetchId !== lastFetchId) return;
|
||||
if (res.success) {
|
||||
setNextRunTime(<span style={{fontSize: 12, color: '#52c41a'}}>{res.msg}</span>)
|
||||
} else {
|
||||
setNextRunTime(<span style={{fontSize: 12, color: '#ff4d4f'}}>{res.msg}</span>)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setNextRunTime(null)
|
||||
}
|
||||
return S.trigger_args[S.trigger];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form layout="vertical" wrapperCol={{span: 14, offset: 5}}>
|
||||
<Form.Item>
|
||||
<Tabs activeKey={trigger} onChange={setTrigger} tabPosition="left" style={{minHeight: 200}}>
|
||||
<Tabs.TabPane tab="普通间隔" key="interval">
|
||||
<Form.Item required label="间隔时间(秒)" extra="每隔指定n秒执行一次。">
|
||||
<InputNumber
|
||||
style={{width: 200}}
|
||||
placeholder="请输入"
|
||||
value={args['interval']}
|
||||
onChange={v => handleArgs('interval', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="一次性" key="date">
|
||||
<Form.Item required label="执行时间" extra="仅在指定时间运行一次。">
|
||||
<DatePicker
|
||||
showTime
|
||||
disabledDate={v => v && v.format('YYYY-MM-DD') < moment().format('YYYY-MM-DD')}
|
||||
style={{width: 200}}
|
||||
placeholder="请选择执行时间"
|
||||
onOk={() => false}
|
||||
value={args['date'] ? moment(args['date']) : undefined}
|
||||
onChange={v => handleArgs('date', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="UNIX Cron" key="cron">
|
||||
<Form.Item required label="执行规则" extra="兼容Cron风格,可参考官方例子">
|
||||
<Input
|
||||
suffix={nextRunTime || <span/>}
|
||||
value={lds.get(args, 'cron.rule')}
|
||||
placeholder="例如每天凌晨1点执行:0 1 * * *"
|
||||
onChange={e => handleCronArgs('rule', e.target.value)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="生效时间" extra="定义的执行规则在到达该时间后生效">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={lds.get(args, 'cron.start') ? moment(args['cron']['start']) : undefined}
|
||||
onChange={v => handleCronArgs('start', v)}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="结束时间" extra="执行规则在到达该时间后不再执行">
|
||||
<DatePicker
|
||||
showTime
|
||||
style={{width: '100%'}}
|
||||
placeholder="可选输入"
|
||||
value={lds.get(args, 'cron.stop') ? moment(args['cron']['stop']) : undefined}
|
||||
onChange={v => handleCronArgs('stop', v)}/>
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{span: 14, offset: 6}}>
|
||||
<Button type="primary" loading={loading} disabled={!args[trigger]} onClick={handleSubmit}>提交</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => store.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Container visible={props.visible} style={{width: 420, margin: '0 auto'}}>
|
||||
<Form layout="vertical" style={{minHeight: 200}}>
|
||||
<Form.Item required label="执行对象">
|
||||
{S.targets.map((id, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Select
|
||||
value={id}
|
||||
showSearch
|
||||
placeholder="请选择"
|
||||
optionFilterProp="children"
|
||||
style={{width: 'calc(100% - 40px)', marginRight: 10, marginBottom: 12}}
|
||||
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
onChange={v => S.editTarget(index, v)}>
|
||||
<Select.Option value="local" disabled={S.targets.includes('local')}>本机</Select.Option>
|
||||
{S.trigger === 'monitor' &&
|
||||
<Select.Option value="monitor" disabled={S.targets.includes('monitor')}>告警关联主机</Select.Option>}
|
||||
{hostStore.rawRecords.map(item => (
|
||||
<Select.Option key={item.id} value={item.id} disabled={S.targets.includes(item.id)}>
|
||||
{`${item.name}(${item['hostname']}:${item['port']})`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{S.targets.length > 1 && (
|
||||
<MinusCircleOutlined className={styles.delIcon} onClick={() => S.delTarget(index)}/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form.Item>
|
||||
<Form.Item extra="本机即Spug服务运行所在的容器或主机。">
|
||||
<Button type="dashed" style={{width: '80%'}} disabled={S.trigger === 'monitor'}
|
||||
onClick={() => setVisible(true)}>
|
||||
<PlusOutlined/>添加执行对象
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={loading} disabled={S.targets.filter(x => x).length === 0} type="primary"
|
||||
onClick={handleSubmit}>提交</Button>
|
||||
<Button style={{marginLeft: 20}} onClick={() => S.page -= 1}>上一步</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<HostSelector
|
||||
visible={visible}
|
||||
selectedRowKeys={[...S.targets]}
|
||||
onCancel={() => setVisible(false)}
|
||||
onOk={(_, ids) => S.targets = ids}/>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -46,6 +46,9 @@ class ComTable extends React.Component {
|
|||
}, {
|
||||
title: '任务类型',
|
||||
dataIndex: 'type',
|
||||
}, {
|
||||
title: '触发方式',
|
||||
dataIndex: 'trigger_alias'
|
||||
}, {
|
||||
title: '最新状态',
|
||||
render: info => {
|
||||
|
|
|
@ -12,4 +12,12 @@
|
|||
|
||||
.delIcon:hover {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
min-height: 200px;
|
||||
|
||||
:global(.ant-tabs-nav-list) {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@ class Store {
|
|||
@observable record = {};
|
||||
@observable page = 0;
|
||||
@observable targets = [undefined];
|
||||
@observable trigger = 'interval';
|
||||
@observable trigger_args = {};
|
||||
@observable isFetching = false;
|
||||
@observable formVisible = false;
|
||||
@observable infoVisible = false;
|
||||
|
@ -56,7 +58,24 @@ class Store {
|
|||
|
||||
showForm = (info) => {
|
||||
this.page = 0;
|
||||
this.record = info || {interpreter: 'sh', rst_notify: {mode: '0'}, trigger: 'interval'};
|
||||
if (info) {
|
||||
this.record = info
|
||||
this.trigger = info.trigger
|
||||
if (info.trigger === 'date') {
|
||||
this.trigger_args = {date: moment(info.trigger_args)}
|
||||
} else if (info.trigger === 'cron') {
|
||||
const args = info.trigger_args
|
||||
if (args.start) args.start = moment(args.start)
|
||||
if (args.stop) args.stop = moment(args.stop)
|
||||
this.trigger_args = {cron: args}
|
||||
} else {
|
||||
this.trigger_args = {[info.trigger]: info.trigger_args}
|
||||
}
|
||||
} else {
|
||||
this.record = {interpreter: 'sh', rst_notify: {mode: '0'}, trigger: 'interval'}
|
||||
this.trigger = 'interval'
|
||||
this.trigger_args = {}
|
||||
}
|
||||
this.formVisible = true
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue