A 新增任务计划触发器"监控告警",可用于故障自愈

4.0
vapao 2022-10-30 09:58:26 +08:00
parent 7e6cba42a0
commit cc4829772e
14 changed files with 413 additions and 237 deletions

View File

@ -3,7 +3,7 @@
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from django_redis import get_redis_connection from django_redis import get_redis_connection
from apps.host.models import Host 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 from socket import socket
import subprocess import subprocess
import platform import platform
@ -105,6 +105,7 @@ def monitor_worker_handler(job):
if not v_time or int(time.time()) - int(v_time) >= quiet * 60: if not v_time or int(time.time()) - int(v_time) >= quiet * 60:
rds.hset(key, f_time, int(time.time())) rds.hset(key, f_time, int(time.time()))
logging.warning('send fault alarm notification') 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) handle_notify(task_id, target, is_ok, message, v_count)

View File

@ -4,6 +4,8 @@
from django.db import close_old_connections from django.db import close_old_connections
from apps.alarm.models import Alarm from apps.alarm.models import Alarm
from apps.monitor.models import Detection from apps.monitor.models import Detection
from apps.schedule.models import Task
from apps.schedule.scheduler import Scheduler
from libs.spug import Notification from libs.spug import Notification
import json import json
@ -41,3 +43,17 @@ def handle_notify(task_id, target, is_ok, out, fault_times):
grp = json.loads(det.notify_grp) grp = json.loads(det.notify_grp)
notify = Notification(grp, event, target, det.name, out, duration) notify = Notification(grp, event, target, det.name, out, duration)
notify.dispatch_monitor(json.loads(det.notify_mode)) 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)

View File

@ -31,9 +31,9 @@ class History(models.Model, ModelMixin):
class Task(models.Model, ModelMixin): class Task(models.Model, ModelMixin):
TRIGGERS = ( TRIGGERS = (
('date', '一次性'), ('date', '一次性'),
('calendarinterval', '日历间隔'),
('cron', 'UNIX cron'), ('cron', 'UNIX cron'),
('interval', '普通间隔') ('interval', '普通间隔'),
('monitor', '监控告警'),
) )
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
type = 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): def to_dict(self, *args, **kwargs):
tmp = super().to_dict(*args, **kwargs) tmp = super().to_dict(*args, **kwargs)
tmp['trigger_alias'] = self.get_trigger_display()
tmp['targets'] = json.loads(self.targets) tmp['targets'] = json.loads(self.targets)
tmp['latest_status'] = self.latest.status if self.latest else None 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_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['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'} 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) tmp['trigger_args'] = json.loads(self.trigger_args)
return tmp return tmp

View File

@ -58,11 +58,8 @@ class Scheduler:
else: else:
raise TypeError(f'unknown schedule policy: {trigger!r}') raise TypeError(f'unknown schedule policy: {trigger!r}')
def _init_builtin_jobs(self): @classmethod
self.scheduler.add_job(auto_run_by_day, 'cron', hour=1, minute=20) def dispatch(cls, task_id, interpreter, command, targets):
self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=1)
def _dispatch(self, task_id, interpreter, command, targets):
output = {x: None for x in targets} output = {x: None for x in targets}
history = History.objects.create( history = History.objects.create(
task_id=task_id, task_id=task_id,
@ -76,14 +73,18 @@ class Scheduler:
rds_cli.rpush(SCHEDULE_WORKER_KEY, json.dumps([history.id, t, interpreter, command])) rds_cli.rpush(SCHEDULE_WORKER_KEY, json.dumps([history.id, t, interpreter, command]))
connections.close_all() 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): def _init(self):
self.scheduler.start() self.scheduler.start()
self._init_builtin_jobs() self._init_builtin_jobs()
try: 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) trigger = self.parse_trigger(task.trigger, task.trigger_args)
self.scheduler.add_job( self.scheduler.add_job(
self._dispatch, self.dispatch,
trigger, trigger,
id=str(task.id), id=str(task.id),
args=(task.id, task.interpreter, task.command, json.loads(task.targets)), args=(task.id, task.interpreter, task.command, json.loads(task.targets)),
@ -103,7 +104,7 @@ class Scheduler:
if task.action in ('add', 'modify'): if task.action in ('add', 'modify'):
trigger = self.parse_trigger(task.trigger, task.trigger_args) trigger = self.parse_trigger(task.trigger, task.trigger_args)
self.scheduler.add_job( self.scheduler.add_job(
self._dispatch, self.dispatch,
trigger, trigger,
id=str(task.id), id=str(task.id),
args=(task.id, task.interpreter, task.command, task.targets), args=(task.id, task.interpreter, task.command, task.targets),

View File

@ -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

View File

@ -16,6 +16,7 @@ import TableCard from './TableCard';
import Breadcrumb from './Breadcrumb'; import Breadcrumb from './Breadcrumb';
import AppSelector from './AppSelector'; import AppSelector from './AppSelector';
import NotFound from './NotFound'; import NotFound from './NotFound';
import Container from './Container';
export { export {
StatisticsCard, StatisticsCard,
@ -31,4 +32,5 @@ export {
Breadcrumb, Breadcrumb,
AppSelector, AppSelector,
NotFound, NotFound,
Container,
} }

View File

@ -42,9 +42,22 @@ class Store {
return records 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 = () => { fetchRecords = () => {
this.isFetching = true; this.isFetching = true;
http.get('/api/monitor/') return http.get('/api/monitor/')
.then(({groups, detections}) => { .then(({groups, detections}) => {
const tmp = new Set(); const tmp = new Set();
detections.map(item => { detections.map(item => {

View File

@ -10,7 +10,7 @@ import Step1 from './Step1';
import Step2 from './Step2'; import Step2 from './Step2';
import Step3 from './Step3'; import Step3 from './Step3';
import store from './store'; import store from './store';
import styles from './index.module.css'; import styles from './index.module.less';
import hostStore from '../host/store'; import hostStore from '../host/store';
export default observer(function () { export default observer(function () {
@ -28,12 +28,12 @@ export default observer(function () {
footer={null}> footer={null}>
<Steps current={store.page} className={styles.steps}> <Steps current={store.page} className={styles.steps}>
<Steps.Step key={0} title="创建任务"/> <Steps.Step key={0} title="创建任务"/>
<Steps.Step key={1} title="选择执行对象"/> <Steps.Step key={1} title="设置触发器"/>
<Steps.Step key={2} title="设置触发器"/> <Steps.Step key={2} title="选择执行对象"/>
</Steps> </Steps>
{store.page === 0 && <Step1/>} <Step1 visible={store.page === 0}/>
{store.page === 1 && <Step2/>} <Step2 visible={store.page === 1}/>
{store.page === 2 && <Step3/>} <Step3 visible={store.page === 2}/>
</Modal> </Modal>
) )
}) })

View File

@ -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 React, { useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Form, Input, Select, Modal, Button, Radio } from 'antd'; import { Form, Input, Select, Modal, Button, Radio } from 'antd';
import { ExclamationCircleOutlined } from '@ant-design/icons'; import { ExclamationCircleOutlined } from '@ant-design/icons';
import { LinkButton, ACEditor } from 'components'; import { LinkButton, ACEditor, Container } from 'components';
import TemplateSelector from '../exec/task/TemplateSelector'; import TemplateSelector from '../exec/task/TemplateSelector';
import { cleanCommand } from 'libs'; import { cleanCommand } from 'libs';
import store from './store'; import store from './store';
export default observer(function () { export default observer(function (props) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [showTmp, setShowTmp] = useState(false); const [showTmp, setShowTmp] = useState(false);
const [command, setCommand] = useState(store.record.command || ''); const [command, setCommand] = useState(store.record.command || '');
@ -68,6 +73,7 @@ export default observer(function () {
} }
return ( return (
<Container visible={props.visible}>
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}> <Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}>
<Form.Item required label="任务类型" style={{marginBottom: 0}}> <Form.Item required label="任务类型" style={{marginBottom: 0}}>
<Form.Item name="type" style={{display: 'inline-block', width: '80%'}}> <Form.Item name="type" style={{display: 'inline-block', width: '80%'}}>
@ -84,7 +90,8 @@ export default observer(function () {
<Form.Item required name="name" label="任务名称"> <Form.Item required name="name" label="任务名称">
<Input placeholder="请输入任务名称"/> <Input placeholder="请输入任务名称"/>
</Form.Item> </Form.Item>
<Form.Item required label="任务内容" extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}> <Form.Item required label="任务内容"
extra={<LinkButton onClick={() => setShowTmp(true)}>从模板添加</LinkButton>}>
<Form.Item noStyle name="interpreter"> <Form.Item noStyle name="interpreter">
<Radio.Group buttonStyle="solid" style={{marginBottom: 12}}> <Radio.Group buttonStyle="solid" style={{marginBottom: 12}}>
<Radio.Button value="sh" style={{width: 80, textAlign: 'center'}}>Shell</Radio.Button> <Radio.Button value="sh" style={{width: 80, textAlign: 'center'}}>Shell</Radio.Button>
@ -128,5 +135,6 @@ export default observer(function () {
</Form.Item> </Form.Item>
{showTmp && <TemplateSelector onOk={handleSelect} onCancel={() => setShowTmp(false)}/>} {showTmp && <TemplateSelector onOk={handleSelect} onCancel={() => setShowTmp(false)}/>}
</Form> </Form>
</Container>
) )
}) })

View File

@ -3,61 +3,182 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { Form, Tabs, DatePicker, InputNumber, Input, Button, Cascader } from 'antd';
import { Form, Select, Button } from 'antd'; import { LoadingOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import HostSelector from 'pages/host/Selector'; import { Container } from 'components';
import store from './store'; import { http, includes } from 'libs';
import hostStore from 'pages/host/store'; import S from './store';
import styles from './index.module.css'; import moment from 'moment';
import lds from 'lodash';
import styles from './index.module.less';
import mStore from '../monitor/store';
export default observer(function () { let lastFetchId = 0;
const [visible, setVisible] = useState(false)
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 ( return (
<React.Fragment> <Container visible={props.visible}>
<Form labelCol={{span: 6}} wrapperCol={{span: 14}} style={{minHeight: 350}}> <Form layout="vertical" wrapperCol={{span: 14, offset: 5}}>
<Form.Item required label="执行对象"> <Form.Item>
{store.targets.map((id, index) => ( <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}> <React.Fragment key={index}>
<Select <Cascader
value={id}
showSearch
placeholder="请选择" placeholder="请选择"
optionFilterProp="children" value={item}
options={mStore.cascaderOptions}
style={{width: '80%', marginRight: 10, marginBottom: 12}} style={{width: '80%', marginRight: 10, marginBottom: 12}}
filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0} showSearch={{filter: (i, p) => p.some(o => includes(o.label, i))}}
onChange={v => store.editTarget(index, v)}> onChange={v => handleMonitorArgs(index, v)}/>
<Select.Option value="local" disabled={store.targets.includes('local')}>本机</Select.Option> {S.trigger_args.monitor?.length > 1 && (
{hostStore.rawRecords.map(item => ( <MinusCircleOutlined className={styles.delIcon} onClick={() => handleMonitorArgs(index)}/>
<Select.Option key={item.id} value={item.id} disabled={store.targets.includes(item.id)}>
{`${item.name}(${item['hostname']}:${item['port']})`}
</Select.Option>
))}
</Select>
{store.targets.length > 1 && (
<MinusCircleOutlined className={styles.delIcon} onClick={() => store.delTarget(index)}/>
)} )}
</React.Fragment> </React.Fragment>
))} ))}
</Form.Item> </Form.Item>
<Form.Item wrapperCol={{span: 14, offset: 6}}> <Form.Item extra="当监控项触发告警时执行。">
<Button type="dashed" style={{width: '80%'}} onClick={() => setVisible(true)}> <Button type="dashed" style={{width: '80%'}} onClick={() => handleMonitorArgs()}>
<PlusOutlined/>添加执行对象 <PlusOutlined/>添加监控项
</Button> </Button>
</Form.Item> </Form.Item>
<HostSelector </Tabs.TabPane>
visible={visible} </Tabs>
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> </Form.Item>
</React.Fragment> <Form.Item wrapperCol={{span: 14, offset: 6}}>
<Button type="primary" disabled={!isValid()} onClick={() => S.page += 1}>下一步</Button>
<Button style={{marginLeft: 20}} onClick={() => S.page -= 1}>上一步</Button>
</Form.Item>
</Form>
</Container>
) )
}) })

View File

@ -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 React, { useState } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Form, Tabs, DatePicker, InputNumber, Input, Button, message } from 'antd'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { LoadingOutlined } from '@ant-design/icons'; import { Form, Select, Button, message } from 'antd';
import { http } from 'libs'; import { Container } from 'components';
import store from './store'; import HostSelector from 'pages/host/Selector';
import hostStore from 'pages/host/store';
import moment from 'moment'; import moment from 'moment';
import lds from 'lodash'; 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 (props) {
const [visible, setVisible] = useState(false)
export default observer(function () { const [loading, setLoading] = useState(false)
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);
function handleSubmit() { function handleSubmit() {
if (trigger === 'date' && args['date'] <= moment()) {
return message.error('任务执行时间不能早于当前时间')
}
setLoading(true) setLoading(true)
const formData = lds.pick(store.record, ['id', 'name', 'type', 'interpreter', 'command', 'desc', 'rst_notify']); const formData = lds.pick(S.record, ['id', 'name', 'type', 'interpreter', 'command', 'desc', 'rst_notify']);
formData['targets'] = store.targets.filter(x => x); formData['targets'] = S.targets.filter(x => x);
formData['trigger'] = trigger; formData['trigger'] = S.trigger;
formData['trigger_args'] = _parse_args(); formData['trigger_args'] = _parse_args();
http.post('/api/schedule/', formData) http.post('/api/schedule/', formData)
.then(res => { .then(res => {
message.success('操作成功'); message.success('操作成功');
store.formVisible = false; S.formVisible = false;
store.fetchRecords() S.fetchRecords()
}, () => setLoading(false)) }, () => 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() { function _parse_args() {
switch (trigger) { switch (S.trigger) {
case 'date': 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': case 'cron':
const {rule, start, stop} = args['cron']; const {rule, start, stop} = S.trigger_args.cron;
return JSON.stringify({ return JSON.stringify({
rule, rule,
start: start ? moment(start).format('YYYY-MM-DD HH:mm:ss') : null, start: start ? moment(start).format('YYYY-MM-DD HH:mm:ss') : null,
stop: stop ? moment(stop).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: default:
return args[trigger]; return S.trigger_args[S.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 ( return (
<Form layout="vertical" wrapperCol={{span: 14, offset: 5}}> <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> <Form.Item>
<Tabs activeKey={trigger} onChange={setTrigger} tabPosition="left" style={{minHeight: 200}}> <Button loading={loading} disabled={S.targets.filter(x => x).length === 0} type="primary"
<Tabs.TabPane tab="普通间隔" key="interval"> onClick={handleSubmit}>提交</Button>
<Form.Item required label="间隔时间(秒)" extra="每隔指定n秒执行一次。"> <Button style={{marginLeft: 20}} onClick={() => S.page -= 1}>上一步</Button>
<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.Item>
</Form> </Form>
<HostSelector
visible={visible}
selectedRowKeys={[...S.targets]}
onCancel={() => setVisible(false)}
onOk={(_, ids) => S.targets = ids}/>
</Container>
) )
}) })

View File

@ -46,6 +46,9 @@ class ComTable extends React.Component {
}, { }, {
title: '任务类型', title: '任务类型',
dataIndex: 'type', dataIndex: 'type',
}, {
title: '触发方式',
dataIndex: 'trigger_alias'
}, { }, {
title: '最新状态', title: '最新状态',
render: info => { render: info => {

View File

@ -13,3 +13,11 @@
.delIcon:hover { .delIcon:hover {
color: #f5222d; color: #f5222d;
} }
.tabs {
min-height: 200px;
:global(.ant-tabs-nav-list) {
align-items: flex-end;
}
}

View File

@ -13,6 +13,8 @@ class Store {
@observable record = {}; @observable record = {};
@observable page = 0; @observable page = 0;
@observable targets = [undefined]; @observable targets = [undefined];
@observable trigger = 'interval';
@observable trigger_args = {};
@observable isFetching = false; @observable isFetching = false;
@observable formVisible = false; @observable formVisible = false;
@observable infoVisible = false; @observable infoVisible = false;
@ -56,7 +58,24 @@ class Store {
showForm = (info) => { showForm = (info) => {
this.page = 0; 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 this.formVisible = true
}; };