spug/spug_api/apps/deploy/views.py

288 lines
13 KiB
Python

# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.views.generic import View
from django.db.models import F
from django.conf import settings
from django.http.response import HttpResponseBadRequest
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, DeployExtend2
from apps.repository.models import Repository
from apps.deploy.utils import dispatch, Helper
from apps.host.models import Host
from collections import defaultdict
from threading import Thread
from datetime import datetime
import subprocess
import json
import os
class RequestView(View):
def get(self, request):
data, query = [], {}
if not request.user.is_supper:
perms = request.user.deploy_perms
query['deploy__app_id__in'] = perms['apps']
query['deploy__env_id__in'] = perms['envs']
for item in DeployRequest.objects.filter(**query).annotate(
env_id=F('deploy__env_id'),
env_name=F('deploy__env__name'),
app_id=F('deploy__app_id'),
app_name=F('deploy__app__name'),
app_host_ids=F('deploy__host_ids'),
app_extend=F('deploy__extend'),
rep_extra=F('repository__extra'),
created_by_user=F('created_by__nickname')):
tmp = item.to_dict()
tmp['env_id'] = item.env_id
tmp['env_name'] = item.env_name
tmp['app_id'] = item.app_id
tmp['app_name'] = item.app_name
tmp['app_extend'] = item.app_extend
tmp['host_ids'] = json.loads(item.host_ids)
tmp['extra'] = json.loads(item.extra) if item.extra else None
tmp['rep_extra'] = json.loads(item.rep_extra) if item.rep_extra else None
tmp['app_host_ids'] = json.loads(item.app_host_ids)
tmp['status_alias'] = item.get_status_display()
tmp['created_by_user'] = item.created_by_user
data.append(tmp)
return json_response(data)
def put(self, request):
form, error = JsonParser(
Argument('id', type=int, help='缺少必要参数'),
Argument('action', filter=lambda x: x in ('check', 'do'), help='参数错误')
).parse(request.body)
if error is None:
req = DeployRequest.objects.filter(pk=form.id).first()
if not req:
return json_response(error='未找到指定发布申请')
pre_req = DeployRequest.objects.filter(
deploy_id=req.deploy_id,
type='1',
id__lt=req.id,
version__isnull=False).first()
if not pre_req:
return json_response(error='未找到该应用可以用于回滚的版本')
if form.action == 'check':
return json_response({'date': pre_req.created_at, 'name': pre_req.name})
DeployRequest.objects.create(
deploy_id=req.deploy_id,
name=f'{req.name} - 回滚',
type='2',
extra=pre_req.extra,
host_ids=req.host_ids,
status='0' if pre_req.deploy.is_audit else '1',
desc='自动回滚至该应用的上个版本',
version=pre_req.version,
created_by=request.user
)
return json_response(error=error)
def delete(self, request):
form, error = JsonParser(
Argument('id', type=int, required=False),
Argument('expire', required=False),
Argument('count', type=int, required=False, help='请输入数字')
).parse(request.GET)
if error is None:
rds = get_redis_connection()
if form.id:
DeployRequest.objects.filter(pk=form.id, status__in=('0', '1', '-1')).delete()
return json_response()
elif form.count:
if form.count < 1:
return json_response(error='请输入正确的保留数量')
counter, ids = defaultdict(int), []
for item in DeployRequest.objects.all():
if counter[item.deploy_id] == form.count:
ids.append(item.id)
else:
counter[item.deploy_id] += 1
count, _ = DeployRequest.objects.filter(id__in=ids).delete()
if ids:
rds.delete(*(f'{settings.REQUEST_KEY}:{x}' for x in ids))
return json_response(count)
elif form.expire:
requests = DeployRequest.objects.filter(created_at__lt=form.expire)
ids = [x.id for x in requests]
count, _ = requests.delete()
if ids:
rds.delete(*(f'{settings.REQUEST_KEY}:{x}' for x in ids))
return json_response(count)
else:
return json_response(error='请至少使用一个删除条件')
return json_response(error=error)
class RequestDetailView(View):
def get(self, request, r_id):
req = DeployRequest.objects.filter(pk=r_id).first()
if not req:
return json_response(error='未找到指定发布申请')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': [f'{human_time()} 读取数据... ']} for x in hosts}
response = {'outputs': outputs, 'status': req.status}
if req.deploy.extend == '2':
outputs['local'] = {'id': 'local', 'data': [f'{human_time()} 读取数据... ']}
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
if not response['h_actions']:
response['outputs'] = {'local': outputs['local']}
rds, key, counter = get_redis_connection(), f'{settings.REQUEST_KEY}:{r_id}', 0
data = rds.lrange(key, counter, counter + 9)
while data:
counter += 10
for item in data:
item = json.loads(item.decode())
if 'data' in item:
outputs[item['key']]['data'].append(item['data'])
if 'step' in item:
outputs[item['key']]['step'] = item['step']
if 'status' in item:
outputs[item['key']]['status'] = item['status']
data = rds.lrange(key, counter, counter + 9)
return json_response(response)
def post(self, request, r_id):
query = {'pk': r_id}
if not request.user.is_supper:
perms = request.user.deploy_perms
query['deploy__app_id__in'] = perms['apps']
query['deploy__env_id__in'] = perms['envs']
req = DeployRequest.objects.filter(**query).first()
if not req:
return json_response(error='未找到指定发布申请')
if req.status not in ('1', '-3'):
return json_response(error='该申请单当前状态还不能执行发布')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
message = f'{human_time()} 等待调度... '
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': [message]} for x in hosts}
req.status = '2'
req.do_at = human_datetime()
req.do_by = request.user
req.save()
Thread(target=dispatch, args=(req,)).start()
if req.deploy.extend == '2':
message = f'{human_time()} 建立连接... '
outputs['local'] = {'id': 'local', 'step': 0, 'data': [message]}
s_actions = json.loads(req.deploy.extend_obj.server_actions)
h_actions = json.loads(req.deploy.extend_obj.host_actions)
if not h_actions:
outputs = {'local': outputs['local']}
return json_response({'s_actions': s_actions, 'h_actions': h_actions, 'outputs': outputs})
return json_response({'outputs': outputs})
def patch(self, request, r_id):
form, error = JsonParser(
Argument('reason', required=False),
Argument('is_pass', type=bool, help='参数错误')
).parse(request.body)
if error is None:
req = DeployRequest.objects.filter(pk=r_id).first()
if not req:
return json_response(error='未找到指定申请')
if not form.is_pass and not form.reason:
return json_response(error='请输入驳回原因')
if req.status != '0':
return json_response(error='该申请当前状态不允许审核')
req.approve_at = human_datetime()
req.approve_by = request.user
req.status = '1' if form.is_pass else '-1'
req.reason = form.reason
req.save()
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_rst')).start()
return json_response(error=error)
def post_request_ext1(request):
form, error = JsonParser(
Argument('id', type=int, required=False),
Argument('name', help='请输申请标题'),
Argument('repository_id', type=int, help='请选择发布版本'),
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
Argument('type', default='1'),
Argument('plan', required=False),
Argument('desc', required=False),
).parse(request.body)
if error is None:
repository = Repository.objects.filter(pk=form.repository_id).first()
if not repository:
return json_response(error='未找到指定构建版本记录')
form.name = form.name.replace("'", '')
form.status = '0' if repository.deploy.is_audit else '1'
form.version = repository.version
form.spug_version = repository.spug_version
form.deploy_id = repository.deploy_id
form.host_ids = json.dumps(sorted(form.host_ids))
if form.id:
req = DeployRequest.objects.get(pk=form.id)
is_required_notify = repository.deploy.is_audit and req.status == '-1'
DeployRequest.objects.filter(pk=form.id).update(created_by=request.user, reason=None, **form)
else:
req = DeployRequest.objects.create(created_by=request.user, **form)
is_required_notify = repository.deploy.is_audit
if is_required_notify:
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_req')).start()
return json_response(error=error)
def post_request_ext2(request):
form, error = JsonParser(
Argument('id', type=int, required=False),
Argument('deploy_id', type=int, help='缺少必要参数'),
Argument('name', help='请输申请标题'),
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择要部署的主机'),
Argument('extra', type=dict, required=False),
Argument('version', default=''),
Argument('plan', required=False),
Argument('desc', required=False),
).parse(request.body)
if error is None:
deploy = Deploy.objects.filter(pk=form.deploy_id).first()
if not deploy:
return json_response(error='未找到该发布配置')
extra = form.pop('extra')
if DeployExtend2.objects.filter(deploy=deploy, host_actions__contains='"src_mode": "1"').exists():
if not extra:
return json_response(error='该应用的发布配置中使用了数据传输动作且设置为发布时上传,请上传要传输的数据')
form.spug_version = extra['path']
form.extra = json.dumps(extra)
else:
form.spug_version = Repository.make_spug_version(deploy.id)
form.name = form.name.replace("'", '')
form.status = '0' if deploy.is_audit else '1'
form.host_ids = json.dumps(form.host_ids)
if form.id:
req = DeployRequest.objects.get(pk=form.id)
is_required_notify = deploy.is_audit and req.status == '-1'
DeployRequest.objects.filter(pk=form.id).update(created_by=request.user, reason=None, **form)
else:
req = DeployRequest.objects.create(created_by=request.user, **form)
is_required_notify = deploy.is_audit
if is_required_notify:
Thread(target=Helper.send_deploy_notify, args=(req, 'approve_req')).start()
return json_response(error=error)
def do_upload(request):
repos_dir = settings.REPOS_DIR
file = request.FILES['file']
deploy_id = request.POST.get('deploy_id')
if file and deploy_id:
dir_name = os.path.join(repos_dir, deploy_id)
file_name = datetime.now().strftime("%Y%m%d%H%M%S")
command = f'mkdir -p {dir_name} && cd {dir_name} && ls | sort -rn | tail -n +11 | xargs rm -rf'
code, outputs = subprocess.getstatusoutput(command)
if code != 0:
return json_response(error=outputs)
with open(os.path.join(dir_name, file_name), 'wb') as f:
for chunk in file.chunks():
f.write(chunk)
return json_response(file_name)
else:
return HttpResponseBadRequest()