A 添加发布日志查看功能

pull/31/head v2.2.0-beta.2
vapao 2020-03-14 15:43:59 +08:00
parent 17dfe2618d
commit 9e6d3b3010
13 changed files with 101 additions and 37 deletions

View File

@ -236,15 +236,15 @@ class Helper:
return files
def send_info(self, key, message):
self.rds.rpush(self.token, json.dumps({'key': key, 'status': 'info', 'data': message}))
self.rds.lpush(self.token, json.dumps({'key': key, 'status': 'info', 'data': message}))
def send_error(self, key, message):
message = '\r\n' + message
self.rds.rpush(self.token, json.dumps({'key': key, 'status': 'error', 'data': message}))
self.rds.lpush(self.token, json.dumps({'key': key, 'status': 'error', 'data': message}))
raise Exception(message)
def send_step(self, key, step, data):
self.rds.rpush(self.token, json.dumps({'key': key, 'step': step, 'data': data}))
self.rds.lpush(self.token, json.dumps({'key': key, 'step': step, 'data': data}))
def local(self, command, env=None):
command = 'set -e\n' + command

View File

@ -3,6 +3,8 @@
# Released under the MIT License.
from django.views.generic import View
from django.db.models import F
from django.conf import settings
from django_redis import get_redis_connection
from libs import json_response, JsonParser, Argument, human_datetime, human_time
from apps.deploy.models import DeployRequest
from apps.app.models import Deploy
@ -112,10 +114,17 @@ class RequestDetailView(View):
return json_response(error='未找到指定发布申请')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
targets = [{'id': x.id, 'title': f'{x.name}({x.hostname}:{x.port})'} for x in hosts]
server_actions, host_actions = [], []
server_actions, host_actions, outputs = [], [], []
if req.deploy.extend == '2':
server_actions = json.loads(req.deploy.extend_obj.server_actions)
host_actions = json.loads(req.deploy.extend_obj.host_actions)
if request.GET.get('log'):
rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
data = rds.lrange(key, counter, counter + 9)
while data:
counter += 10
outputs.extend(x.decode() for x in data)
data = rds.lrange(key, counter, counter + 9)
return json_response({
'app_name': req.deploy.app.name,
'env_name': req.deploy.env.name,
@ -124,7 +133,8 @@ class RequestDetailView(View):
'status_alias': req.get_status_display(),
'targets': targets,
'server_actions': server_actions,
'host_actions': host_actions
'host_actions': host_actions,
'outputs': outputs
})
def post(self, request, r_id):

View File

@ -109,7 +109,7 @@ class Scheduler:
rds_cli.delete(settings.MONITOR_KEY)
logger.info('Running monitor')
while True:
_, data = rds_cli.blpop(settings.MONITOR_KEY)
_, data = rds_cli.brpop(settings.MONITOR_KEY)
task = AttrDict(json.loads(data))
if task.action in ('add', 'modify'):
trigger = IntervalTrigger(minutes=int(task.rate), timezone=self.timezone)

View File

@ -40,13 +40,13 @@ class DetectionView(View):
if task and task.is_active:
form.action = 'modify'
rds_cli = get_redis_connection()
rds_cli.rpush(settings.MONITOR_KEY, json.dumps(form))
rds_cli.lpush(settings.MONITOR_KEY, json.dumps(form))
else:
dtt = Detection.objects.create(created_by=request.user, **form)
form.action = 'add'
form.id = dtt.id
rds_cli = get_redis_connection()
rds_cli.rpush(settings.MONITOR_KEY, json.dumps(form))
rds_cli.lpush(settings.MONITOR_KEY, json.dumps(form))
return json_response(error=error)
def patch(self, request):
@ -64,7 +64,7 @@ class DetectionView(View):
else:
message = {'id': form.id, 'action': 'remove'}
rds_cli = get_redis_connection()
rds_cli.rpush(settings.MONITOR_KEY, json.dumps(message))
rds_cli.lpush(settings.MONITOR_KEY, json.dumps(message))
return json_response(error=error)
def delete(self, request):

View File

@ -87,7 +87,7 @@ class Scheduler:
rds_cli.delete(settings.SCHEDULE_KEY)
logger.info('Running scheduler')
while True:
_, data = rds_cli.blpop(settings.SCHEDULE_KEY)
_, data = rds_cli.brpop(settings.SCHEDULE_KEY)
task = AttrDict(json.loads(data))
if task.action in ('add', 'modify'):
trigger = self.parse_trigger(task.trigger, task.trigger_args)

View File

@ -40,7 +40,7 @@ class Schedule(View):
form.action = 'modify'
form.targets = json.loads(form.targets)
rds_cli = get_redis_connection()
rds_cli.rpush(settings.SCHEDULE_KEY, json.dumps(form))
rds_cli.lpush(settings.SCHEDULE_KEY, json.dumps(form))
else:
Task.objects.create(created_by=request.user, **form)
return json_response(error=error)
@ -60,7 +60,7 @@ class Schedule(View):
else:
message = {'id': form.id, 'action': 'remove'}
rds_cli = get_redis_connection()
rds_cli.rpush(settings.SCHEDULE_KEY, json.dumps(message))
rds_cli.lpush(settings.SCHEDULE_KEY, json.dumps(message))
return json_response(error=error)
def delete(self, request):

View File

@ -3,17 +3,24 @@
# Released under the MIT License.
from channels.generic.websocket import WebsocketConsumer
from django_redis import get_redis_connection
from django.conf import settings
from apps.setting.utils import AppSetting
from apps.host.models import Host
from threading import Thread
from urllib.parse import parse_qs
import json
class ExecConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
query = parse_qs(self.scope['query_string'].decode())
e_id = query.get('id', [None])[0]
self.token = self.scope['url_route']['kwargs']['token']
self.log_key = f'{settings.REQUEST_KEY}:{e_id}' if e_id else None
self.rds = get_redis_connection()
if self.log_key:
self.rds.delete(self.log_key)
def connect(self):
self.accept()
@ -21,11 +28,18 @@ class ExecConsumer(WebsocketConsumer):
def disconnect(self, code):
self.rds.close()
def get_response(self):
if self.log_key:
return self.rds.brpoplpush(self.token, self.log_key, timeout=5)
else:
return self.rds.brpop(self.token, timeout=5)[1]
def receive(self, **kwargs):
response = self.rds.blpop(self.token, timeout=5)
response = self.get_response()
while response:
self.send(text_data=response[1].decode())
response = self.rds.blpop(self.token, timeout=5)
data = response.decode()
self.send(text_data=data)
response = self.get_response()
self.send(text_data='pong')

View File

@ -28,7 +28,7 @@ class Job:
def _send(self, message, with_expire=False):
if self.rds_cli is None:
self.rds_cli = get_redis_connection()
self.rds_cli.rpush(self.token, json.dumps(message))
self.rds_cli.lpush(self.token, json.dumps(message))
if with_expire:
self.rds_cli.expire(self.token, 300)

View File

@ -100,6 +100,7 @@ TEMPLATES = [
SCHEDULE_KEY = 'spug:schedule'
MONITOR_KEY = 'spug:monitor'
REQUEST_KEY = 'spug:request'
REPOS_DIR = os.path.join(BASE_DIR, 'repos')
# Internationalization

View File

@ -26,8 +26,19 @@ class Ext1Index extends React.Component {
componentDidMount() {
this.id = this.props.match.params.id;
http.get(`/api/deploy/request/${this.id}/`)
.then(res => store.request = res)
this.log = this.props.match.params.log;
http.get(`/api/deploy/request/${this.id}/`, {params: {log: this.log}})
.then(res => {
store.request = res;
while (res.outputs.length) {
const msg = JSON.parse(res.outputs.pop());
if (!store.outputs.hasOwnProperty(msg.key)) {
const data = msg.key === 'local' ? '读取数据... ' : '';
store.outputs[msg.key] = {data}
}
this._parse_message(msg)
}
})
.finally(() => this.setState({fetching: false}))
}
@ -37,6 +48,13 @@ class Ext1Index extends React.Component {
store.outputs = {};
}
_parse_message = (message) => {
const {key, data, step, status} = message;
if (data !== undefined) store.outputs[key]['data'] += data;
if (step !== undefined) store.outputs[key]['step'] = step;
if (status !== undefined) store.outputs[key]['status'] = status;
};
handleDeploy = () => {
this.setState({loading: true});
http.post(`/api/deploy/request/${this.id}/`)
@ -44,7 +62,7 @@ class Ext1Index extends React.Component {
store.request.status = '2';
store.outputs = outputs;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/`);
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/?id=${this.id}`);
this.socket.onopen = () => {
this.socket.send('ok');
};
@ -52,10 +70,7 @@ class Ext1Index extends React.Component {
if (e.data === 'pong') {
this.socket.send('ping')
} else {
const {key, data, step, status} = JSON.parse(e.data);
if (data !== undefined) store.outputs[key]['data'] += data;
if (step !== undefined) store.outputs[key]['step'] = step;
if (status !== undefined) store.outputs[key]['status'] = status;
this._parse_message(JSON.parse(e.data))
}
}
})
@ -100,7 +115,8 @@ class Ext1Index extends React.Component {
subTitle={`${app_name} - ${env_name}`}
style={{padding: 0}}
tags={this.getStatusAlias()}
extra={<Button loading={this.state.loading} type="primary" disabled={!['1', '-3'].includes(status)}
extra={<Button loading={this.state.loading} type="primary"
disabled={this.log || !['1', '-3'].includes(status)}
onClick={this.handleDeploy}>发布</Button>}
onBack={() => history.goBack()}/>
<Collapse defaultActiveKey={1} className={styles.collapse}>

View File

@ -26,8 +26,19 @@ class Ext1Index extends React.Component {
componentDidMount() {
this.id = this.props.match.params.id;
http.get(`/api/deploy/request/${this.id}/`)
.then(res => store.request = res)
this.log = this.props.match.params.log;
http.get(`/api/deploy/request/${this.id}/`, {params: {log: this.log}})
.then(res => {
store.request = res;
while (res.outputs.length) {
const msg = JSON.parse(res.outputs.pop());
if (!store.outputs.hasOwnProperty(msg.key)) {
const data = msg.key === 'local' ? '读取数据... ' : '';
store.outputs[msg.key] = {data}
}
this._parse_message(msg)
}
})
.finally(() => this.setState({fetching: false}))
}
@ -37,6 +48,13 @@ class Ext1Index extends React.Component {
store.outputs = {};
}
_parse_message = (message) => {
const {key, data, step, status} = message;
if (data !== undefined) store.outputs[key]['data'] += data;
if (step !== undefined) store.outputs[key]['step'] = step;
if (status !== undefined) store.outputs[key]['status'] = status;
};
handleDeploy = () => {
this.setState({loading: true});
http.post(`/api/deploy/request/${this.id}/`)
@ -44,7 +62,7 @@ class Ext1Index extends React.Component {
store.request.status = '2';
store.outputs = outputs;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/`);
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/?id=${this.id}`);
this.socket.onopen = () => {
this.socket.send('ok');
};
@ -52,10 +70,7 @@ class Ext1Index extends React.Component {
if (e.data === 'pong') {
this.socket.send('ping')
} else {
const {key, data, step, status} = JSON.parse(e.data);
if (data !== undefined) store.outputs[key]['data'] += data;
if (step !== undefined) store.outputs[key]['step'] = step;
if (status !== undefined) store.outputs[key]['status'] = status;
this._parse_message(JSON.parse(e.data))
}
}
})
@ -100,7 +115,7 @@ class Ext1Index extends React.Component {
subTitle={`${app_name} - ${env_name}`}
style={{padding: 0}}
tags={this.getStatusAlias()}
extra={<Button loading={this.state.loading} type="primary" disabled={!['1', '-3'].includes(status)}
extra={<Button loading={this.state.loading} type="primary" disabled={this.log || !['1', '-3'].includes(status)}
onClick={this.handleDeploy}>发布</Button>}
onBack={() => history.goBack()}/>
<Collapse defaultActiveKey={1} className={styles.collapse}>

View File

@ -85,6 +85,8 @@ class ComTable extends React.Component {
switch (info.status) {
case '-3':
return <React.Fragment>
<AuthLink auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}/1`}>查看</AuthLink>
<Divider type="vertical"/>
<AuthLink auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}`}>发布</AuthLink>
<Divider type="vertical"/>
<LinkButton
@ -94,11 +96,15 @@ class ComTable extends React.Component {
onClick={() => this.handleRollback(info)}>回滚</LinkButton>
</React.Fragment>;
case '3':
return <LinkButton
auth="deploy.request.do"
disabled={info.type === '2'}
loading={this.state.loading}
onClick={() => this.handleRollback(info)}>回滚</LinkButton>;
return <React.Fragment>
<AuthLink auth="deploy.request.do" to={`/deploy/do/ext${info['app_extend']}/${info.id}/1`}>查看</AuthLink>
<Divider type="vertical"/>
<LinkButton
auth="deploy.request.do"
disabled={info.type === '2'}
loading={this.state.loading}
onClick={() => this.handleRollback(info)}>回滚</LinkButton>
</React.Fragment>;
case '-1':
return <React.Fragment>
<LinkButton auth="deploy.request.edit" onClick={() => store.showForm(info)}>编辑</LinkButton>

View File

@ -15,4 +15,6 @@ export default [
makeRoute('/request', request),
makeRoute('/do/ext1/:id', doExt1Index),
makeRoute('/do/ext2/:id', doExt2Index),
makeRoute('/do/ext1/:id/:log', doExt1Index),
makeRoute('/do/ext2/:id/:log', doExt2Index),
]