diff --git a/spug_api/apps/monitor/executors.py b/spug_api/apps/monitor/executors.py index 5630d68..2d3ec5d 100644 --- a/spug_api/apps/monitor/executors.py +++ b/spug_api/apps/monitor/executors.py @@ -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) diff --git a/spug_api/apps/monitor/utils.py b/spug_api/apps/monitor/utils.py index 2231e10..5bb0585 100644 --- a/spug_api/apps/monitor/utils.py +++ b/spug_api/apps/monitor/utils.py @@ -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) diff --git a/spug_api/apps/schedule/models.py b/spug_api/apps/schedule/models.py index 31f1221..30ee246 100644 --- a/spug_api/apps/schedule/models.py +++ b/spug_api/apps/schedule/models.py @@ -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 diff --git a/spug_api/apps/schedule/scheduler.py b/spug_api/apps/schedule/scheduler.py index d723c66..ebf12a8 100644 --- a/spug_api/apps/schedule/scheduler.py +++ b/spug_api/apps/schedule/scheduler.py @@ -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), diff --git a/spug_web/src/components/Container.js b/spug_web/src/components/Container.js new file mode 100644 index 0000000..e36e072 --- /dev/null +++ b/spug_web/src/components/Container.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react' + + +function Container(props) { + const {visible, style, className} = props; + + return ( +
+ {props.children} +
+ ) +} + +Container.defaultProps = { + visible: true +} + +export default Container diff --git a/spug_web/src/components/index.js b/spug_web/src/components/index.js index e140894..2145b0b 100644 --- a/spug_web/src/components/index.js +++ b/spug_web/src/components/index.js @@ -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, } diff --git a/spug_web/src/pages/monitor/store.js b/spug_web/src/pages/monitor/store.js index 79c7ec9..6e6083c 100644 --- a/spug_web/src/pages/monitor/store.js +++ b/spug_web/src/pages/monitor/store.js @@ -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 => { diff --git a/spug_web/src/pages/schedule/Form.js b/spug_web/src/pages/schedule/Form.js index de662cf..9d99e66 100644 --- a/spug_web/src/pages/schedule/Form.js +++ b/spug_web/src/pages/schedule/Form.js @@ -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}> - - + + - {store.page === 0 && } - {store.page === 1 && } - {store.page === 2 && } + + + ) }) diff --git a/spug_web/src/pages/schedule/Step1.js b/spug_web/src/pages/schedule/Step1.js index 516533c..f7bd802 100644 --- a/spug_web/src/pages/schedule/Step1.js +++ b/spug_web/src/pages/schedule/Step1.js @@ -1,13 +1,18 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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 ( -
- - - + + + + + + + + + - - + + - - - - - setShowTmp(true)}>从模板添加}> - - - Shell - Python - + setShowTmp(true)}>从模板添加}> + + + Shell + Python + + + + {({getFieldValue}) => ( + + )} + - - {({getFieldValue}) => ( - - )} - - - + 任务执行失败告警通知, 钉钉收不到通知? )}> - store.record.rst_notify.value = e.target.value} - addonBefore={( - - )} - disabled={store.record.rst_notify.mode === '0'} - placeholder={modePlaceholder}/> - - - - - - {() => } - - {showTmp && setShowTmp(false)}/>} - + store.record.rst_notify.value = e.target.value} + addonBefore={( + + )} + disabled={store.record.rst_notify.mode === '0'} + placeholder={modePlaceholder}/> + + + + + + {() => } + + {showTmp && setShowTmp(false)}/>} + + ) }) \ No newline at end of file diff --git a/spug_web/src/pages/schedule/Step2.js b/spug_web/src/pages/schedule/Step2.js index e092573..d360ba2 100644 --- a/spug_web/src/pages/schedule/Step2.js +++ b/spug_web/src/pages/schedule/Step2.js @@ -3,61 +3,182 @@ * Copyright (c) * 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(); + 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({res.msg}) + } else { + setNextRunTime({res.msg}) + } + }) + } else { + setNextRunTime(null) + } + } + } return ( - -
- - {store.targets.map((id, index) => ( - - } + value={S.trigger_args.cron?.rule} + placeholder="例如每天凌晨1点执行:0 1 * * *" + onChange={e => handleCronArgs('rule', e.target.value)}/> + + + handleCronArgs('start', v)}/> + + + handleCronArgs('stop', v)}/> + + + + + {lds.get(S.trigger_args, 'monitor', [[]]).map((item, index) => ( + + p.some(o => includes(o.label, i))}} + onChange={v => handleMonitorArgs(index, v)}/> + {S.trigger_args.monitor?.length > 1 && ( + handleMonitorArgs(index)}/> + )} + ))} - - {store.targets.length > 1 && ( - store.delTarget(index)}/> - )} - - ))} + + + + + + - + + - setVisible(false)} - onOk={(_, ids) => store.targets = ids}/> - - - - -
+ ) -}) +}) \ No newline at end of file diff --git a/spug_web/src/pages/schedule/Step3.js b/spug_web/src/pages/schedule/Step3.js index c8a340e..65abf05 100644 --- a/spug_web/src/pages/schedule/Step3.js +++ b/spug_web/src/pages/schedule/Step3.js @@ -1,143 +1,103 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * 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(); - 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({res.msg}) - } else { - setNextRunTime({res.msg}) - } - }) - } else { - setNextRunTime(null) - } + return S.trigger_args[S.trigger]; } } return ( -
- - - - - handleArgs('interval', 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)}/> - - - - - } - value={lds.get(args, 'cron.rule')} - placeholder="例如每天凌晨1点执行:0 1 * * *" - onChange={e => handleCronArgs('rule', e.target.value)}/> - - - handleCronArgs('start', v)}/> - - - handleCronArgs('stop', v)}/> - - - - - - - - -
+ +
+ + {S.targets.map((id, index) => ( + + + {S.targets.length > 1 && ( + S.delTarget(index)}/> + )} + + ))} + + + + + + + + +
+ setVisible(false)} + onOk={(_, ids) => S.targets = ids}/> +
) -}) \ No newline at end of file +}) diff --git a/spug_web/src/pages/schedule/Table.js b/spug_web/src/pages/schedule/Table.js index 5979cc0..f001538 100644 --- a/spug_web/src/pages/schedule/Table.js +++ b/spug_web/src/pages/schedule/Table.js @@ -46,6 +46,9 @@ class ComTable extends React.Component { }, { title: '任务类型', dataIndex: 'type', + }, { + title: '触发方式', + dataIndex: 'trigger_alias' }, { title: '最新状态', render: info => { diff --git a/spug_web/src/pages/schedule/index.module.css b/spug_web/src/pages/schedule/index.module.less similarity index 63% rename from spug_web/src/pages/schedule/index.module.css rename to spug_web/src/pages/schedule/index.module.less index dd9a3e0..fc320e3 100644 --- a/spug_web/src/pages/schedule/index.module.css +++ b/spug_web/src/pages/schedule/index.module.less @@ -12,4 +12,12 @@ .delIcon:hover { color: #f5222d; +} + +.tabs { + min-height: 200px; + + :global(.ant-tabs-nav-list) { + align-items: flex-end; + } } \ No newline at end of file diff --git a/spug_web/src/pages/schedule/store.js b/spug_web/src/pages/schedule/store.js index cd4c864..5bc934e 100644 --- a/spug_web/src/pages/schedule/store.js +++ b/spug_web/src/pages/schedule/store.js @@ -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 };