mirror of https://github.com/openspug/spug
A 新增任务计划触发器"监控告警",可用于故障自愈
parent
7e6cba42a0
commit
cc4829772e
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
|
@ -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 => {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue