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 return files
def send_info(self, key, message): 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): def send_error(self, key, message):
message = '\r\n' + 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) raise Exception(message)
def send_step(self, key, step, data): 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): def local(self, command, env=None):
command = 'set -e\n' + command command = 'set -e\n' + command

View File

@ -3,6 +3,8 @@
# Released under the MIT License. # Released under the MIT License.
from django.views.generic import View from django.views.generic import View
from django.db.models import F 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 libs import json_response, JsonParser, Argument, human_datetime, human_time
from apps.deploy.models import DeployRequest from apps.deploy.models import DeployRequest
from apps.app.models import Deploy from apps.app.models import Deploy
@ -112,10 +114,17 @@ class RequestDetailView(View):
return json_response(error='未找到指定发布申请') return json_response(error='未找到指定发布申请')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids)) 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] 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': if req.deploy.extend == '2':
server_actions = json.loads(req.deploy.extend_obj.server_actions) server_actions = json.loads(req.deploy.extend_obj.server_actions)
host_actions = json.loads(req.deploy.extend_obj.host_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({ return json_response({
'app_name': req.deploy.app.name, 'app_name': req.deploy.app.name,
'env_name': req.deploy.env.name, 'env_name': req.deploy.env.name,
@ -124,7 +133,8 @@ class RequestDetailView(View):
'status_alias': req.get_status_display(), 'status_alias': req.get_status_display(),
'targets': targets, 'targets': targets,
'server_actions': server_actions, 'server_actions': server_actions,
'host_actions': host_actions 'host_actions': host_actions,
'outputs': outputs
}) })
def post(self, request, r_id): def post(self, request, r_id):

View File

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

View File

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

View File

@ -87,7 +87,7 @@ class Scheduler:
rds_cli.delete(settings.SCHEDULE_KEY) rds_cli.delete(settings.SCHEDULE_KEY)
logger.info('Running scheduler') logger.info('Running scheduler')
while True: while True:
_, data = rds_cli.blpop(settings.SCHEDULE_KEY) _, data = rds_cli.brpop(settings.SCHEDULE_KEY)
task = AttrDict(json.loads(data)) task = AttrDict(json.loads(data))
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)

View File

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

View File

@ -3,17 +3,24 @@
# Released under the MIT License. # Released under the MIT License.
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from django_redis import get_redis_connection from django_redis import get_redis_connection
from django.conf import settings
from apps.setting.utils import AppSetting from apps.setting.utils import AppSetting
from apps.host.models import Host from apps.host.models import Host
from threading import Thread from threading import Thread
from urllib.parse import parse_qs
import json import json
class ExecConsumer(WebsocketConsumer): class ExecConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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.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() self.rds = get_redis_connection()
if self.log_key:
self.rds.delete(self.log_key)
def connect(self): def connect(self):
self.accept() self.accept()
@ -21,11 +28,18 @@ class ExecConsumer(WebsocketConsumer):
def disconnect(self, code): def disconnect(self, code):
self.rds.close() 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): def receive(self, **kwargs):
response = self.rds.blpop(self.token, timeout=5) response = self.get_response()
while response: while response:
self.send(text_data=response[1].decode()) data = response.decode()
response = self.rds.blpop(self.token, timeout=5) self.send(text_data=data)
response = self.get_response()
self.send(text_data='pong') self.send(text_data='pong')

View File

@ -28,7 +28,7 @@ class Job:
def _send(self, message, with_expire=False): def _send(self, message, with_expire=False):
if self.rds_cli is None: if self.rds_cli is None:
self.rds_cli = get_redis_connection() 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: if with_expire:
self.rds_cli.expire(self.token, 300) self.rds_cli.expire(self.token, 300)

View File

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

View File

@ -26,8 +26,19 @@ class Ext1Index extends React.Component {
componentDidMount() { componentDidMount() {
this.id = this.props.match.params.id; this.id = this.props.match.params.id;
http.get(`/api/deploy/request/${this.id}/`) this.log = this.props.match.params.log;
.then(res => store.request = res) 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})) .finally(() => this.setState({fetching: false}))
} }
@ -37,6 +48,13 @@ class Ext1Index extends React.Component {
store.outputs = {}; 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 = () => { handleDeploy = () => {
this.setState({loading: true}); this.setState({loading: true});
http.post(`/api/deploy/request/${this.id}/`) http.post(`/api/deploy/request/${this.id}/`)
@ -44,7 +62,7 @@ class Ext1Index extends React.Component {
store.request.status = '2'; store.request.status = '2';
store.outputs = outputs; store.outputs = outputs;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 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.onopen = () => {
this.socket.send('ok'); this.socket.send('ok');
}; };
@ -52,10 +70,7 @@ class Ext1Index extends React.Component {
if (e.data === 'pong') { if (e.data === 'pong') {
this.socket.send('ping') this.socket.send('ping')
} else { } else {
const {key, data, step, status} = JSON.parse(e.data); this._parse_message(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;
} }
} }
}) })
@ -100,7 +115,8 @@ class Ext1Index extends React.Component {
subTitle={`${app_name} - ${env_name}`} subTitle={`${app_name} - ${env_name}`}
style={{padding: 0}} style={{padding: 0}}
tags={this.getStatusAlias()} 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>} onClick={this.handleDeploy}>发布</Button>}
onBack={() => history.goBack()}/> onBack={() => history.goBack()}/>
<Collapse defaultActiveKey={1} className={styles.collapse}> <Collapse defaultActiveKey={1} className={styles.collapse}>

View File

@ -26,8 +26,19 @@ class Ext1Index extends React.Component {
componentDidMount() { componentDidMount() {
this.id = this.props.match.params.id; this.id = this.props.match.params.id;
http.get(`/api/deploy/request/${this.id}/`) this.log = this.props.match.params.log;
.then(res => store.request = res) 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})) .finally(() => this.setState({fetching: false}))
} }
@ -37,6 +48,13 @@ class Ext1Index extends React.Component {
store.outputs = {}; 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 = () => { handleDeploy = () => {
this.setState({loading: true}); this.setState({loading: true});
http.post(`/api/deploy/request/${this.id}/`) http.post(`/api/deploy/request/${this.id}/`)
@ -44,7 +62,7 @@ class Ext1Index extends React.Component {
store.request.status = '2'; store.request.status = '2';
store.outputs = outputs; store.outputs = outputs;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 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.onopen = () => {
this.socket.send('ok'); this.socket.send('ok');
}; };
@ -52,10 +70,7 @@ class Ext1Index extends React.Component {
if (e.data === 'pong') { if (e.data === 'pong') {
this.socket.send('ping') this.socket.send('ping')
} else { } else {
const {key, data, step, status} = JSON.parse(e.data); this._parse_message(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;
} }
} }
}) })
@ -100,7 +115,7 @@ class Ext1Index extends React.Component {
subTitle={`${app_name} - ${env_name}`} subTitle={`${app_name} - ${env_name}`}
style={{padding: 0}} style={{padding: 0}}
tags={this.getStatusAlias()} 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>} onClick={this.handleDeploy}>发布</Button>}
onBack={() => history.goBack()}/> onBack={() => history.goBack()}/>
<Collapse defaultActiveKey={1} className={styles.collapse}> <Collapse defaultActiveKey={1} className={styles.collapse}>

View File

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

View File

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