add planned deploy

pull/330/head
vapao 2021-04-26 17:30:20 +08:00
parent a28300a641
commit 0e6e618cef
8 changed files with 55 additions and 12 deletions

View File

@ -32,6 +32,7 @@ class DeployRequest(models.Model, ModelMixin):
reason = models.CharField(max_length=255, null=True) reason = models.CharField(max_length=255, null=True)
version = models.CharField(max_length=50, null=True) version = models.CharField(max_length=50, null=True)
spug_version = models.CharField(max_length=50, null=True) spug_version = models.CharField(max_length=50, null=True)
plan = models.DateTimeField(null=True)
created_at = models.CharField(max_length=20, default=human_datetime) created_at = models.CharField(max_length=20, default=human_datetime)
created_by = models.ForeignKey(User, models.PROTECT, related_name='+') created_by = models.ForeignKey(User, models.PROTECT, related_name='+')

View File

@ -201,6 +201,7 @@ def post_request_ext1(request):
Argument('repository_id', type=int, help='请选择发布版本'), Argument('repository_id', type=int, help='请选择发布版本'),
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'), Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
Argument('type', default='1'), Argument('type', default='1'),
Argument('plan', required=False),
Argument('desc', required=False), Argument('desc', required=False),
).parse(request.body) ).parse(request.body)
if error is None: if error is None:
@ -232,7 +233,8 @@ def post_request_ext2(request):
Argument('name', help='请输申请标题'), Argument('name', help='请输申请标题'),
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'), Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
Argument('extra', type=dict, required=False), Argument('extra', type=dict, required=False),
Argument('version', required=False), Argument('version', default=''),
Argument('plan', required=False),
Argument('desc', required=False), Argument('desc', required=False),
).parse(request.body) ).parse(request.body)
if error is None: if error is None:

View File

@ -6,8 +6,10 @@ from apps.account.models import History
from apps.alarm.models import Alarm from apps.alarm.models import Alarm
from apps.schedule.models import Task from apps.schedule.models import Task
from apps.deploy.models import DeployRequest from apps.deploy.models import DeployRequest
from libs.utils import parse_time from apps.deploy.utils import dispatch
from libs.utils import parse_time, human_datetime
from datetime import datetime, timedelta from datetime import datetime, timedelta
from threading import Thread
def auto_run_by_day(): def auto_run_by_day():
@ -24,8 +26,15 @@ def auto_run_by_day():
def auto_run_by_minute(): def auto_run_by_minute():
close_old_connections()
now = datetime.now() now = datetime.now()
for req in DeployRequest.objects.filter(status='2'): for req in DeployRequest.objects.filter(status='2'):
if (now - parse_time(req.do_at)).seconds > 3600: if (now - parse_time(req.do_at)).seconds > 3600:
req.status = '-3' req.status = '-3'
req.save() req.save()
for req in DeployRequest.objects.filter(status='1', plan__lte=now):
req.status = '2'
req.do_at = human_datetime()
req.do_by = req.created_by
req.save()
Thread(target=dispatch, args=(req,)).start()

View File

@ -78,7 +78,7 @@ class Scheduler:
def _init_builtin_jobs(self): 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_day, 'cron', hour=1, minute=20)
self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=5) self.scheduler.add_job(auto_run_by_minute, 'interval', minutes=1)
def _dispatch(self, task_id, command, targets): def _dispatch(self, task_id, command, targets):
close_old_connections() close_old_connections()

View File

@ -5,7 +5,7 @@
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, message, Button } from 'antd'; import { Modal, Form, Input, Select, DatePicker, message, Button } from 'antd';
import HostSelector from './HostSelector'; import HostSelector from './HostSelector';
import hostStore from 'pages/host/store'; import hostStore from 'pages/host/store';
import http from 'libs/http'; import http from 'libs/http';
@ -19,6 +19,7 @@ export default observer(function () {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [versions, setVersions] = useState([]); const [versions, setVersions] = useState([]);
const [host_ids, setHostIds] = useState([]); const [host_ids, setHostIds] = useState([]);
const [plan, setPlan] = useState(store.record.plan);
useEffect(() => { useEffect(() => {
const {deploy_id, app_host_ids, host_ids} = store.record; const {deploy_id, app_host_ids, host_ids} = store.record;
@ -38,6 +39,7 @@ export default observer(function () {
formData['host_ids'] = host_ids; formData['host_ids'] = host_ids;
formData['type'] = store.record.type; formData['type'] = store.record.type;
formData['deploy_id'] = store.record.deploy_id; formData['deploy_id'] = store.record.deploy_id;
if (plan) formData.plan = plan.format('YYYY-MM-DD HH:mm:00');
http.post('/api/deploy/request/ext1/', formData) http.post('/api/deploy/request/ext1/', formData)
.then(res => { .then(res => {
message.success('操作成功'); message.success('操作成功');
@ -46,7 +48,7 @@ export default observer(function () {
}, () => setLoading(false)) }, () => setLoading(false))
} }
const {app_host_ids, type, rb_id,} = store.record; const {app_host_ids, type, rb_id} = store.record;
return ( return (
<Modal <Modal
visible visible
@ -56,7 +58,7 @@ export default observer(function () {
onCancel={() => store.ext1Visible = false} onCancel={() => store.ext1Visible = false}
confirmLoading={loading} confirmLoading={loading}
onOk={handleSubmit}> onOk={handleSubmit}>
<Form form={form} initialValues={store.record} labelCol={{span: 5}} wrapperCol={{span: 17}}> <Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 16}}>
<Form.Item required name="name" label="申请标题"> <Form.Item required name="name" label="申请标题">
<Input placeholder="请输入申请标题"/> <Input placeholder="请输入申请标题"/>
</Form.Item> </Form.Item>
@ -79,6 +81,18 @@ export default observer(function () {
<Form.Item name="desc" label="备注信息"> <Form.Item name="desc" label="备注信息">
<Input placeholder="请输入备注信息"/> <Input placeholder="请输入备注信息"/>
</Form.Item> </Form.Item>
{type !== '2' && (
<Form.Item label="定时发布" tooltip="在到达指定时间后自动发布会有最多1分钟的延迟。">
<DatePicker
showTime
value={plan}
style={{width: 180}}
format="YYYY-MM-DD HH:mm"
placeholder="请设置发布时间"
onChange={setPlan}/>
{plan ? <span style={{marginLeft: 24, fontSize: 12, color: '#888'}}>大约 {plan.fromNow()}</span> : null}
</Form.Item>
)}
</Form> </Form>
{visible && <HostSelector {visible && <HostSelector
host_ids={host_ids} host_ids={host_ids}

View File

@ -6,7 +6,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import { Modal, Form, Input, Upload, message, Button } from 'antd'; import { Modal, Form, Input, Upload, DatePicker, message, Button } from 'antd';
import hostStore from 'pages/host/store'; import hostStore from 'pages/host/store';
import HostSelector from './HostSelector'; import HostSelector from './HostSelector';
import { http, X_TOKEN } from 'libs'; import { http, X_TOKEN } from 'libs';
@ -21,6 +21,7 @@ export default observer(function () {
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [fileList, setFileList] = useState([]); const [fileList, setFileList] = useState([]);
const [host_ids, setHostIds] = useState([]); const [host_ids, setHostIds] = useState([]);
const [plan, setPlan] = useState(store.record.plan);
useEffect(() => { useEffect(() => {
const {app_host_ids, host_ids, extra} = store.record; const {app_host_ids, host_ids, extra} = store.record;
@ -38,6 +39,7 @@ export default observer(function () {
formData['id'] = store.record.id; formData['id'] = store.record.id;
formData['host_ids'] = host_ids; formData['host_ids'] = host_ids;
formData['deploy_id'] = store.record.deploy_id; formData['deploy_id'] = store.record.deploy_id;
if (plan) formData.plan = plan.format('YYYY-MM-DD HH:mm:00');
if (fileList.length > 0) formData['extra'] = lds.pick(fileList[0], ['path', 'name']); if (fileList.length > 0) formData['extra'] = lds.pick(fileList[0], ['path', 'name']);
http.post('/api/deploy/request/ext2/', formData) http.post('/api/deploy/request/ext2/', formData)
.then(res => { .then(res => {
@ -67,17 +69,17 @@ export default observer(function () {
return false return false
} }
const {app_host_ids, deploy_id} = store.record; const {app_host_ids, deploy_id, type} = store.record;
return ( return (
<Modal <Modal
visible visible
width={800} width={600}
maskClosable={false} maskClosable={false}
title={`${store.record.id ? '编辑' : '新建'}发布申请`} title={`${store.record.id ? '编辑' : '新建'}发布申请`}
onCancel={() => store.ext2Visible = false} onCancel={() => store.ext2Visible = false}
confirmLoading={loading} confirmLoading={loading}
onOk={handleSubmit}> onOk={handleSubmit}>
<Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 14}}> <Form form={form} initialValues={store.record} labelCol={{span: 6}} wrapperCol={{span: 16}}>
<Form.Item required name="name" label="申请标题"> <Form.Item required name="name" label="申请标题">
<Input placeholder="请输入申请标题"/> <Input placeholder="请输入申请标题"/>
</Form.Item> </Form.Item>
@ -100,6 +102,18 @@ export default observer(function () {
<Form.Item name="desc" label="备注信息"> <Form.Item name="desc" label="备注信息">
<Input placeholder="请输入备注信息"/> <Input placeholder="请输入备注信息"/>
</Form.Item> </Form.Item>
{type !== '2' && (
<Form.Item label="定时发布" tooltip="在到达指定时间后自动发布会有最多1分钟的延迟。">
<DatePicker
showTime
value={plan}
style={{width: 180}}
format="YYYY-MM-DD HH:mm"
placeholder="请设置发布时间"
onChange={setPlan}/>
{plan ? <span style={{marginLeft: 24, fontSize: 12, color: '#888'}}>大约 {plan.fromNow()}</span> : null}
</Form.Item>
)}
</Form> </Form>
{visible && <HostSelector {visible && <HostSelector
host_ids={host_ids} host_ids={host_ids}

View File

@ -6,7 +6,7 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined } from '@ant-design/icons'; import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined } from '@ant-design/icons';
import { Radio, Modal, Popover, Tag, Popconfirm, message } from 'antd'; import { Radio, Modal, Popover, Tag, Popconfirm, Tooltip, message } from 'antd';
import { http, hasPermission } from 'libs'; import { http, hasPermission } from 'libs';
import { Action, AuthButton, TableCard } from 'components'; import { Action, AuthButton, TableCard } from 'components';
import styles from './index.module.less'; import styles from './index.module.less';
@ -17,7 +17,8 @@ function ComTable() {
title: '申请标题', title: '申请标题',
render: info => ( render: info => (
<div> <div>
{info.type === '2' && <Tag color="#f50">R</Tag>} {info.type === '2' && <Tooltip title="回滚发布"><Tag color="#f50">R</Tag></Tooltip>}
{info.plan && <Tooltip title={`定时发布(${info.plan}`}> <Tag color="#108ee9">P</Tag></Tooltip>}
{info.name} {info.name}
</div> </div>
) )

View File

@ -5,6 +5,7 @@
*/ */
import { observable, computed } from "mobx"; import { observable, computed } from "mobx";
import http from 'libs/http'; import http from 'libs/http';
import moment from 'moment';
import lds from 'lodash'; import lds from 'lodash';
class Store { class Store {
@ -106,6 +107,7 @@ class Store {
showForm = (info) => { showForm = (info) => {
this.record = info; this.record = info;
if (info.plan) this.record.plan = moment(info.plan);
if (info['app_extend'] === '1') { if (info['app_extend'] === '1') {
this.ext1Visible = true this.ext1Visible = true
} else { } else {