mirror of https://github.com/openspug/spug
add planned deploy
parent
a28300a641
commit
0e6e618cef
|
@ -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='+')
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue