mirror of https://github.com/openspug/spug
fix issue
parent
083452dd90
commit
6c77c699aa
|
@ -0,0 +1,212 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django_redis import get_redis_connection
|
||||||
|
from django.conf import settings
|
||||||
|
from libs.utils import human_datetime
|
||||||
|
from apps.host.models import Host
|
||||||
|
from apps.notify.models import Notify
|
||||||
|
import requests
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class SpugError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Helper:
|
||||||
|
def __init__(self, rds, key):
|
||||||
|
self.rds = rds
|
||||||
|
self.key = key
|
||||||
|
self.rds.delete(self.key)
|
||||||
|
self.by_deploy = key.startswith(settings.REQUEST_KEY)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _make_dd_notify(cls, action, req, version, host_str):
|
||||||
|
texts = [
|
||||||
|
f'**申请标题:** {req.name}',
|
||||||
|
f'**应用名称:** {req.deploy.app.name}',
|
||||||
|
f'**应用版本:** {version}',
|
||||||
|
f'**发布环境:** {req.deploy.env.name}',
|
||||||
|
f'**发布主机:** {host_str}',
|
||||||
|
]
|
||||||
|
if action == 'approve_req':
|
||||||
|
texts.insert(0, '## %s ## ' % '发布审核申请')
|
||||||
|
texts.extend([
|
||||||
|
f'**申请人员:** {req.created_by.nickname}',
|
||||||
|
f'**申请时间:** {human_datetime()}',
|
||||||
|
'> 来自 Spug运维平台'
|
||||||
|
])
|
||||||
|
elif action == 'approve_rst':
|
||||||
|
color, text = ('#008000', '通过') if req.status == '1' else ('#f90202', '驳回')
|
||||||
|
texts.insert(0, '## %s ## ' % '发布审核结果')
|
||||||
|
texts.extend([
|
||||||
|
f'**审核人员:** {req.approve_by.nickname}',
|
||||||
|
f'**审核结果:** <font color="{color}">{text}</font>',
|
||||||
|
f'**审核意见:** {req.reason or ""}',
|
||||||
|
f'**审核时间:** {human_datetime()}',
|
||||||
|
'> 来自 Spug运维平台'
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
color, text = ('#008000', '成功') if req.status == '3' else ('#f90202', '失败')
|
||||||
|
texts.insert(0, '## %s ## ' % '发布结果通知')
|
||||||
|
if req.approve_at:
|
||||||
|
texts.append(f'**审核人员:** {req.approve_by.nickname}')
|
||||||
|
do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
|
||||||
|
texts.extend([
|
||||||
|
f'**执行人员:** {do_user}',
|
||||||
|
f'**发布结果:** <font color="{color}">{text}</font>',
|
||||||
|
f'**发布时间:** {human_datetime()}',
|
||||||
|
'> 来自 Spug运维平台'
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
'msgtype': 'markdown',
|
||||||
|
'markdown': {
|
||||||
|
'title': 'Spug 发布消息通知',
|
||||||
|
'text': '\n\n'.join(texts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _make_wx_notify(cls, action, req, version, host_str):
|
||||||
|
texts = [
|
||||||
|
f'申请标题: {req.name}',
|
||||||
|
f'应用名称: {req.deploy.app.name}',
|
||||||
|
f'应用版本: {version}',
|
||||||
|
f'发布环境: {req.deploy.env.name}',
|
||||||
|
f'发布主机: {host_str}',
|
||||||
|
]
|
||||||
|
|
||||||
|
if action == 'approve_req':
|
||||||
|
texts.insert(0, '## %s' % '发布审核申请')
|
||||||
|
texts.extend([
|
||||||
|
f'申请人员: {req.created_by.nickname}',
|
||||||
|
f'申请时间: {human_datetime()}',
|
||||||
|
'> 来自 Spug运维平台'
|
||||||
|
])
|
||||||
|
elif action == 'approve_rst':
|
||||||
|
color, text = ('info', '通过') if req.status == '1' else ('warning', '驳回')
|
||||||
|
texts.insert(0, '## %s' % '发布审核结果')
|
||||||
|
texts.extend([
|
||||||
|
f'审核人员: {req.approve_by.nickname}',
|
||||||
|
f'审核结果: <font color="{color}">{text}</font>',
|
||||||
|
f'审核意见: {req.reason or ""}',
|
||||||
|
f'审核时间: {human_datetime()}',
|
||||||
|
'> 来自 Spug运维平台'
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
color, text = ('info', '成功') if req.status == '3' else ('warning', '失败')
|
||||||
|
texts.insert(0, '## %s' % '发布结果通知')
|
||||||
|
if req.approve_at:
|
||||||
|
texts.append(f'审核人员: {req.approve_by.nickname}')
|
||||||
|
do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
|
||||||
|
texts.extend([
|
||||||
|
f'执行人员: {do_user}',
|
||||||
|
f'发布结果: <font color="{color}">{text}</font>',
|
||||||
|
f'发布时间: {human_datetime()}',
|
||||||
|
'> 来自 Spug运维平台'
|
||||||
|
])
|
||||||
|
return {
|
||||||
|
'msgtype': 'markdown',
|
||||||
|
'markdown': {
|
||||||
|
'content': '\n'.join(texts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_deploy_notify(cls, req, action=None):
|
||||||
|
rst_notify = json.loads(req.deploy.rst_notify)
|
||||||
|
host_ids = json.loads(req.host_ids)
|
||||||
|
if rst_notify['mode'] != '0' and rst_notify.get('value'):
|
||||||
|
version = req.version
|
||||||
|
hosts = [{'id': x.id, 'name': x.name} for x in Host.objects.filter(id__in=host_ids)]
|
||||||
|
host_str = ', '.join(x['name'] for x in hosts[:2])
|
||||||
|
if len(hosts) > 2:
|
||||||
|
host_str += f'等{len(hosts)}台主机'
|
||||||
|
if rst_notify['mode'] == '1':
|
||||||
|
data = cls._make_dd_notify(action, req, version, host_str)
|
||||||
|
elif rst_notify['mode'] == '2':
|
||||||
|
data = {
|
||||||
|
'action': action,
|
||||||
|
'req_id': req.id,
|
||||||
|
'req_name': req.name,
|
||||||
|
'app_id': req.deploy.app_id,
|
||||||
|
'app_name': req.deploy.app.name,
|
||||||
|
'env_id': req.deploy.env_id,
|
||||||
|
'env_name': req.deploy.env.name,
|
||||||
|
'status': req.status,
|
||||||
|
'reason': req.reason,
|
||||||
|
'version': version,
|
||||||
|
'targets': hosts,
|
||||||
|
'is_success': req.status == '3',
|
||||||
|
'created_at': human_datetime()
|
||||||
|
}
|
||||||
|
elif rst_notify['mode'] == '3':
|
||||||
|
data = cls._make_wx_notify(action, req, version, host_str)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
res = requests.post(rst_notify['value'], json=data)
|
||||||
|
if res.status_code != 200:
|
||||||
|
Notify.make_notify('flag', '1', '发布通知发送失败', f'返回状态码:{res.status_code}, 请求URL:{res.url}')
|
||||||
|
if rst_notify['mode'] in ['1', '3']:
|
||||||
|
res = res.json()
|
||||||
|
if res.get('errcode') != 0:
|
||||||
|
Notify.make_notify('flag', '1', '发布通知发送失败', f'返回数据:{res}')
|
||||||
|
|
||||||
|
def parse_filter_rule(self, data: str, sep='\n'):
|
||||||
|
data, files = data.strip(), []
|
||||||
|
if data:
|
||||||
|
for line in data.split(sep):
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
files.append(line)
|
||||||
|
return files
|
||||||
|
|
||||||
|
def _send(self, message):
|
||||||
|
self.rds.rpush(self.key, json.dumps(message))
|
||||||
|
|
||||||
|
def send_info(self, key, message):
|
||||||
|
if message:
|
||||||
|
self._send({'key': key, 'data': message})
|
||||||
|
|
||||||
|
def send_error(self, key, message, with_break=True):
|
||||||
|
message = f'\r\n\033[31m{message}\033[0m'
|
||||||
|
self._send({'key': key, 'status': 'error', 'data': message})
|
||||||
|
if with_break:
|
||||||
|
raise SpugError
|
||||||
|
|
||||||
|
def send_step(self, key, step, data):
|
||||||
|
self._send({'key': key, 'step': step, 'data': data})
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
# save logs for two weeks
|
||||||
|
self.rds.expire(self.key, 14 * 24 * 60 * 60)
|
||||||
|
self.rds.close()
|
||||||
|
|
||||||
|
def local(self, command, env=None):
|
||||||
|
if env:
|
||||||
|
env = dict(env.items())
|
||||||
|
env.update(os.environ)
|
||||||
|
task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
while True:
|
||||||
|
message = task.stdout.readline()
|
||||||
|
if not message:
|
||||||
|
break
|
||||||
|
message = message.decode().rstrip('\r\n')
|
||||||
|
self.send_info('local', message + '\r\n')
|
||||||
|
if task.wait() != 0:
|
||||||
|
self.send_error('local', f'exit code: {task.returncode}')
|
||||||
|
|
||||||
|
def remote(self, key, ssh, command, env=None):
|
||||||
|
code = -1
|
||||||
|
for code, out in ssh.exec_command_with_stream(command, environment=env):
|
||||||
|
self.send_info(key, out)
|
||||||
|
if code != 0:
|
||||||
|
self.send_error(key, f'exit code: {code}')
|
||||||
|
|
||||||
|
def remote_raw(self, key, ssh, command):
|
||||||
|
code, out = ssh.exec_command_raw(command)
|
||||||
|
if code != 0:
|
||||||
|
self.send_error(key, f'exit code: {code}')
|
|
@ -6,6 +6,7 @@ from libs import ModelMixin, human_datetime
|
||||||
from apps.account.models import User
|
from apps.account.models import User
|
||||||
from apps.app.models import Deploy
|
from apps.app.models import Deploy
|
||||||
from apps.repository.models import Repository
|
from apps.repository.models import Repository
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class DeployRequest(models.Model, ModelMixin):
|
class DeployRequest(models.Model, ModelMixin):
|
||||||
|
@ -42,6 +43,13 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
do_at = models.CharField(max_length=20, null=True)
|
do_at = models.CharField(max_length=20, null=True)
|
||||||
do_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True)
|
do_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_quick_deploy(self):
|
||||||
|
if self.extra:
|
||||||
|
extra = json.loads(self.extra)
|
||||||
|
return extra[0] in ('branch', 'tag')
|
||||||
|
return False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<DeployRequest name={self.name}>'
|
return f'<DeployRequest name={self.name}>'
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
# Released under the AGPL-3.0 License.
|
# Released under the AGPL-3.0 License.
|
||||||
from django_redis import get_redis_connection
|
from django_redis import get_redis_connection
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from libs.utils import AttrDict, human_time, human_datetime
|
from django.db import close_old_connections
|
||||||
|
from libs.utils import AttrDict, human_time
|
||||||
from apps.host.models import Host
|
from apps.host.models import Host
|
||||||
from apps.notify.models import Notify
|
|
||||||
from apps.config.utils import compose_configs
|
from apps.config.utils import compose_configs
|
||||||
|
from apps.repository.models import Repository
|
||||||
|
from apps.repository.utils import dispatch as build_repository
|
||||||
|
from apps.deploy.helper import Helper, SpugError
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
import requests
|
|
||||||
import subprocess
|
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
|
@ -17,17 +18,13 @@ import os
|
||||||
REPOS_DIR = settings.REPOS_DIR
|
REPOS_DIR = settings.REPOS_DIR
|
||||||
|
|
||||||
|
|
||||||
class SpugError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def dispatch(req):
|
def dispatch(req):
|
||||||
rds = get_redis_connection()
|
rds = get_redis_connection()
|
||||||
rds_key = f'{settings.REQUEST_KEY}:{req.id}'
|
rds_key = f'{settings.REQUEST_KEY}:{req.id}'
|
||||||
|
helper = Helper(rds, rds_key)
|
||||||
try:
|
try:
|
||||||
api_token = uuid.uuid4().hex
|
api_token = uuid.uuid4().hex
|
||||||
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
rds.setex(api_token, 60 * 60, f'{req.deploy.app_id},{req.deploy.env_id}')
|
||||||
helper = Helper(rds, rds_key)
|
|
||||||
env = AttrDict(
|
env = AttrDict(
|
||||||
SPUG_APP_NAME=req.deploy.app.name,
|
SPUG_APP_NAME=req.deploy.app.name,
|
||||||
SPUG_APP_ID=str(req.deploy.app_id),
|
SPUG_APP_ID=str(req.deploy.app_id),
|
||||||
|
@ -55,13 +52,25 @@ def dispatch(req):
|
||||||
req.status = '-3'
|
req.status = '-3'
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
rds.expire(rds_key, 14 * 24 * 60 * 60)
|
close_old_connections()
|
||||||
rds.close()
|
|
||||||
req.save()
|
req.save()
|
||||||
|
helper.clear()
|
||||||
Helper.send_deploy_notify(req)
|
Helper.send_deploy_notify(req)
|
||||||
|
|
||||||
|
|
||||||
def _ext1_deploy(req, helper, env):
|
def _ext1_deploy(req, helper, env):
|
||||||
|
if not req.repository_id:
|
||||||
|
rep = Repository(
|
||||||
|
app_id=req.deploy.app_id,
|
||||||
|
env_id=req.deploy.env_id,
|
||||||
|
deploy_id=req.deploy_id,
|
||||||
|
version=req.version,
|
||||||
|
spug_version=req.spug_version,
|
||||||
|
extra=req.extra,
|
||||||
|
created_by_id=req.created_by_id
|
||||||
|
)
|
||||||
|
build_repository(rep, helper)
|
||||||
|
req.repository = rep
|
||||||
extend = req.deploy.extend_obj
|
extend = req.deploy.extend_obj
|
||||||
env.update(SPUG_DST_DIR=extend.dst_dir)
|
env.update(SPUG_DST_DIR=extend.dst_dir)
|
||||||
threads, latest_exception = [], None
|
threads, latest_exception = [], None
|
||||||
|
@ -83,7 +92,7 @@ def _ext1_deploy(req, helper, env):
|
||||||
|
|
||||||
|
|
||||||
def _ext2_deploy(req, helper, env):
|
def _ext2_deploy(req, helper, env):
|
||||||
helper.send_info('local', f'完成\r\n')
|
helper.send_info('local', f'\033[32m完成√\033[0m\r\n')
|
||||||
extend, step = req.deploy.extend_obj, 1
|
extend, step = req.deploy.extend_obj, 1
|
||||||
host_actions = json.loads(extend.host_actions)
|
host_actions = json.loads(extend.host_actions)
|
||||||
server_actions = json.loads(extend.server_actions)
|
server_actions = json.loads(extend.server_actions)
|
||||||
|
@ -125,7 +134,7 @@ def _ext2_deploy(req, helper, env):
|
||||||
exclude = ' '.join(excludes)
|
exclude = ' '.join(excludes)
|
||||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||||
helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
|
helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
|
||||||
helper.send_info('local', f'{human_time()} 打包完成\r\n')
|
helper.send_info('local', f'{human_time()} \033[32m完成√\033[0m\r\n')
|
||||||
tmp_transfer_file = os.path.join(sp_dir, tar_gz_file)
|
tmp_transfer_file = os.path.join(sp_dir, tar_gz_file)
|
||||||
break
|
break
|
||||||
if host_actions:
|
if host_actions:
|
||||||
|
@ -153,7 +162,7 @@ def _ext2_deploy(req, helper, env):
|
||||||
|
|
||||||
def _deploy_ext1_host(req, helper, h_id, env):
|
def _deploy_ext1_host(req, helper, h_id, env):
|
||||||
extend = req.deploy.extend_obj
|
extend = req.deploy.extend_obj
|
||||||
helper.send_step(h_id, 1, f'就绪\r\n{human_time()} 数据准备... ')
|
helper.send_step(h_id, 1, f'\033[32m就绪√\033[0m\r\n{human_time()} 数据准备... ')
|
||||||
host = Host.objects.filter(pk=h_id).first()
|
host = Host.objects.filter(pk=h_id).first()
|
||||||
if not host:
|
if not host:
|
||||||
helper.send_error(h_id, 'no such host')
|
helper.send_error(h_id, 'no such host')
|
||||||
|
@ -175,7 +184,7 @@ def _deploy_ext1_host(req, helper, h_id, env):
|
||||||
|
|
||||||
command = f'cd {extend.dst_repo} && rm -rf {req.spug_version} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
command = f'cd {extend.dst_repo} && rm -rf {req.spug_version} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
||||||
helper.remote_raw(host.id, ssh, command)
|
helper.remote_raw(host.id, ssh, command)
|
||||||
helper.send_step(h_id, 1, '完成\r\n')
|
helper.send_step(h_id, 1, '\033[32m完成√\033[0m\r\n')
|
||||||
|
|
||||||
# pre host
|
# pre host
|
||||||
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||||
|
@ -187,7 +196,7 @@ def _deploy_ext1_host(req, helper, h_id, env):
|
||||||
# do deploy
|
# do deploy
|
||||||
helper.send_step(h_id, 3, f'{human_time()} 执行发布... ')
|
helper.send_step(h_id, 3, f'{human_time()} 执行发布... ')
|
||||||
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
||||||
helper.send_step(h_id, 3, '完成\r\n')
|
helper.send_step(h_id, 3, '\033[32m完成√\033[0m\r\n')
|
||||||
|
|
||||||
# post host
|
# post host
|
||||||
if extend.hook_post_host:
|
if extend.hook_post_host:
|
||||||
|
@ -195,11 +204,11 @@ def _deploy_ext1_host(req, helper, h_id, env):
|
||||||
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
|
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
|
||||||
helper.remote(host.id, ssh, command)
|
helper.remote(host.id, ssh, command)
|
||||||
|
|
||||||
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
|
helper.send_step(h_id, 100, f'\r\n{human_time()} ** \033[32m发布成功\033[0m **')
|
||||||
|
|
||||||
|
|
||||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||||
helper.send_info(h_id, '就绪\r\n')
|
helper.send_info(h_id, '\033[32m就绪√\033[0m\r\n')
|
||||||
host = Host.objects.filter(pk=h_id).first()
|
host = Host.objects.filter(pk=h_id).first()
|
||||||
if not host:
|
if not host:
|
||||||
helper.send_error(h_id, 'no such host')
|
helper.send_error(h_id, 'no such host')
|
||||||
|
@ -230,194 +239,4 @@ def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||||
command = f'cd /tmp ; {action["data"]}'
|
command = f'cd /tmp ; {action["data"]}'
|
||||||
helper.remote(host.id, ssh, command)
|
helper.remote(host.id, ssh, command)
|
||||||
|
|
||||||
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
|
helper.send_step(h_id, 100, f'\r\n{human_time()} ** \033[32m发布成功\033[0m **')
|
||||||
|
|
||||||
|
|
||||||
class Helper:
|
|
||||||
def __init__(self, rds, key):
|
|
||||||
self.rds = rds
|
|
||||||
self.key = key
|
|
||||||
self.rds.delete(self.key)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _make_dd_notify(cls, action, req, version, host_str):
|
|
||||||
texts = [
|
|
||||||
f'**申请标题:** {req.name}',
|
|
||||||
f'**应用名称:** {req.deploy.app.name}',
|
|
||||||
f'**应用版本:** {version}',
|
|
||||||
f'**发布环境:** {req.deploy.env.name}',
|
|
||||||
f'**发布主机:** {host_str}',
|
|
||||||
]
|
|
||||||
if action == 'approve_req':
|
|
||||||
texts.insert(0, '## %s ## ' % '发布审核申请')
|
|
||||||
texts.extend([
|
|
||||||
f'**申请人员:** {req.created_by.nickname}',
|
|
||||||
f'**申请时间:** {human_datetime()}',
|
|
||||||
'> 来自 Spug运维平台'
|
|
||||||
])
|
|
||||||
elif action == 'approve_rst':
|
|
||||||
color, text = ('#008000', '通过') if req.status == '1' else ('#f90202', '驳回')
|
|
||||||
texts.insert(0, '## %s ## ' % '发布审核结果')
|
|
||||||
texts.extend([
|
|
||||||
f'**审核人员:** {req.approve_by.nickname}',
|
|
||||||
f'**审核结果:** <font color="{color}">{text}</font>',
|
|
||||||
f'**审核意见:** {req.reason or ""}',
|
|
||||||
f'**审核时间:** {human_datetime()}',
|
|
||||||
'> 来自 Spug运维平台'
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
color, text = ('#008000', '成功') if req.status == '3' else ('#f90202', '失败')
|
|
||||||
texts.insert(0, '## %s ## ' % '发布结果通知')
|
|
||||||
if req.approve_at:
|
|
||||||
texts.append(f'**审核人员:** {req.approve_by.nickname}')
|
|
||||||
do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
|
|
||||||
texts.extend([
|
|
||||||
f'**执行人员:** {do_user}',
|
|
||||||
f'**发布结果:** <font color="{color}">{text}</font>',
|
|
||||||
f'**发布时间:** {human_datetime()}',
|
|
||||||
'> 来自 Spug运维平台'
|
|
||||||
])
|
|
||||||
return {
|
|
||||||
'msgtype': 'markdown',
|
|
||||||
'markdown': {
|
|
||||||
'title': 'Spug 发布消息通知',
|
|
||||||
'text': '\n\n'.join(texts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _make_wx_notify(cls, action, req, version, host_str):
|
|
||||||
texts = [
|
|
||||||
f'申请标题: {req.name}',
|
|
||||||
f'应用名称: {req.deploy.app.name}',
|
|
||||||
f'应用版本: {version}',
|
|
||||||
f'发布环境: {req.deploy.env.name}',
|
|
||||||
f'发布主机: {host_str}',
|
|
||||||
]
|
|
||||||
|
|
||||||
if action == 'approve_req':
|
|
||||||
texts.insert(0, '## %s' % '发布审核申请')
|
|
||||||
texts.extend([
|
|
||||||
f'申请人员: {req.created_by.nickname}',
|
|
||||||
f'申请时间: {human_datetime()}',
|
|
||||||
'> 来自 Spug运维平台'
|
|
||||||
])
|
|
||||||
elif action == 'approve_rst':
|
|
||||||
color, text = ('info', '通过') if req.status == '1' else ('warning', '驳回')
|
|
||||||
texts.insert(0, '## %s' % '发布审核结果')
|
|
||||||
texts.extend([
|
|
||||||
f'审核人员: {req.approve_by.nickname}',
|
|
||||||
f'审核结果: <font color="{color}">{text}</font>',
|
|
||||||
f'审核意见: {req.reason or ""}',
|
|
||||||
f'审核时间: {human_datetime()}',
|
|
||||||
'> 来自 Spug运维平台'
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
color, text = ('info', '成功') if req.status == '3' else ('warning', '失败')
|
|
||||||
texts.insert(0, '## %s' % '发布结果通知')
|
|
||||||
if req.approve_at:
|
|
||||||
texts.append(f'审核人员: {req.approve_by.nickname}')
|
|
||||||
do_user = req.do_by.nickname if req.type != '3' else 'Webhook'
|
|
||||||
texts.extend([
|
|
||||||
f'执行人员: {do_user}',
|
|
||||||
f'发布结果: <font color="{color}">{text}</font>',
|
|
||||||
f'发布时间: {human_datetime()}',
|
|
||||||
'> 来自 Spug运维平台'
|
|
||||||
])
|
|
||||||
return {
|
|
||||||
'msgtype': 'markdown',
|
|
||||||
'markdown': {
|
|
||||||
'content': '\n'.join(texts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def send_deploy_notify(cls, req, action=None):
|
|
||||||
rst_notify = json.loads(req.deploy.rst_notify)
|
|
||||||
host_ids = json.loads(req.host_ids)
|
|
||||||
if rst_notify['mode'] != '0' and rst_notify.get('value'):
|
|
||||||
version = req.version
|
|
||||||
hosts = [{'id': x.id, 'name': x.name} for x in Host.objects.filter(id__in=host_ids)]
|
|
||||||
host_str = ', '.join(x['name'] for x in hosts[:2])
|
|
||||||
if len(hosts) > 2:
|
|
||||||
host_str += f'等{len(hosts)}台主机'
|
|
||||||
if rst_notify['mode'] == '1':
|
|
||||||
data = cls._make_dd_notify(action, req, version, host_str)
|
|
||||||
elif rst_notify['mode'] == '2':
|
|
||||||
data = {
|
|
||||||
'action': action,
|
|
||||||
'req_id': req.id,
|
|
||||||
'req_name': req.name,
|
|
||||||
'app_id': req.deploy.app_id,
|
|
||||||
'app_name': req.deploy.app.name,
|
|
||||||
'env_id': req.deploy.env_id,
|
|
||||||
'env_name': req.deploy.env.name,
|
|
||||||
'status': req.status,
|
|
||||||
'reason': req.reason,
|
|
||||||
'version': version,
|
|
||||||
'targets': hosts,
|
|
||||||
'is_success': req.status == '3',
|
|
||||||
'created_at': human_datetime()
|
|
||||||
}
|
|
||||||
elif rst_notify['mode'] == '3':
|
|
||||||
data = cls._make_wx_notify(action, req, version, host_str)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
res = requests.post(rst_notify['value'], json=data)
|
|
||||||
if res.status_code != 200:
|
|
||||||
Notify.make_notify('flag', '1', '发布通知发送失败', f'返回状态码:{res.status_code}, 请求URL:{res.url}')
|
|
||||||
if rst_notify['mode'] in ['1', '3']:
|
|
||||||
res = res.json()
|
|
||||||
if res.get('errcode') != 0:
|
|
||||||
Notify.make_notify('flag', '1', '发布通知发送失败', f'返回数据:{res}')
|
|
||||||
|
|
||||||
def parse_filter_rule(self, data: str, sep='\n'):
|
|
||||||
data, files = data.strip(), []
|
|
||||||
if data:
|
|
||||||
for line in data.split(sep):
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#'):
|
|
||||||
files.append(line)
|
|
||||||
return files
|
|
||||||
|
|
||||||
def _send(self, message):
|
|
||||||
self.rds.rpush(self.key, json.dumps(message))
|
|
||||||
|
|
||||||
def send_info(self, key, message):
|
|
||||||
if message:
|
|
||||||
self._send({'key': key, 'data': message})
|
|
||||||
|
|
||||||
def send_error(self, key, message, with_break=True):
|
|
||||||
message = '\r\n' + message
|
|
||||||
self._send({'key': key, 'status': 'error', 'data': message})
|
|
||||||
if with_break:
|
|
||||||
raise SpugError
|
|
||||||
|
|
||||||
def send_step(self, key, step, data):
|
|
||||||
self._send({'key': key, 'step': step, 'data': data})
|
|
||||||
|
|
||||||
def local(self, command, env=None):
|
|
||||||
if env:
|
|
||||||
env = dict(env.items())
|
|
||||||
env.update(os.environ)
|
|
||||||
task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
||||||
while True:
|
|
||||||
message = task.stdout.readline()
|
|
||||||
if not message:
|
|
||||||
break
|
|
||||||
message = message.decode().rstrip('\r\n')
|
|
||||||
self.send_info('local', message + '\r\n')
|
|
||||||
if task.wait() != 0:
|
|
||||||
self.send_error('local', f'exit code: {task.returncode}')
|
|
||||||
|
|
||||||
def remote(self, key, ssh, command, env=None):
|
|
||||||
code = -1
|
|
||||||
for code, out in ssh.exec_command_with_stream(command, environment=env):
|
|
||||||
self.send_info(key, out)
|
|
||||||
if code != 0:
|
|
||||||
self.send_error(key, f'exit code: {code}')
|
|
||||||
|
|
||||||
def remote_raw(self, key, ssh, command):
|
|
||||||
code, out = ssh.exec_command_raw(command)
|
|
||||||
if code != 0:
|
|
||||||
self.send_error(key, f'exit code: {code}')
|
|
||||||
|
|
|
@ -126,8 +126,11 @@ class RequestDetailView(View):
|
||||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
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}
|
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': f'{human_time()} 读取数据... '} for x in hosts}
|
||||||
response = {'outputs': outputs, 'status': req.status}
|
response = {'outputs': outputs, 'status': req.status}
|
||||||
|
if req.is_quick_deploy:
|
||||||
|
outputs['local'] = {'id': 'local', 'data': ''}
|
||||||
if req.deploy.extend == '2':
|
if req.deploy.extend == '2':
|
||||||
outputs['local'] = {'id': 'local', 'data': f'{human_time()} 读取数据... '}
|
outputs['local'] = {'id': 'local', 'data': f'{human_time()} 读取数据... '}
|
||||||
|
if req.deploy.extend == '2':
|
||||||
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
|
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
|
||||||
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
|
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
|
||||||
if not response['h_actions']:
|
if not response['h_actions']:
|
||||||
|
@ -146,6 +149,11 @@ class RequestDetailView(View):
|
||||||
outputs[item['key']]['status'] = item['status']
|
outputs[item['key']]['status'] = item['status']
|
||||||
data = rds.lrange(key, counter, counter + 9)
|
data = rds.lrange(key, counter, counter + 9)
|
||||||
response['index'] = counter
|
response['index'] = counter
|
||||||
|
if req.is_quick_deploy:
|
||||||
|
if outputs['local']['data']:
|
||||||
|
outputs['local']['data'] = f'{human_time()} 读取数据... ' + outputs['local']['data']
|
||||||
|
else:
|
||||||
|
outputs['local'].update(step=100, data=f'{human_time()} 已构建完成忽略执行。')
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
|
||||||
def post(self, request, r_id):
|
def post(self, request, r_id):
|
||||||
|
@ -167,9 +175,14 @@ class RequestDetailView(View):
|
||||||
req.do_by = request.user
|
req.do_by = request.user
|
||||||
req.save()
|
req.save()
|
||||||
Thread(target=dispatch, args=(req,)).start()
|
Thread(target=dispatch, args=(req,)).start()
|
||||||
|
if req.is_quick_deploy:
|
||||||
|
if req.repository_id:
|
||||||
|
outputs['local'] = {'id': 'local', 'step': 100, 'data': f'{human_time()} 已构建完成忽略执行。'}
|
||||||
|
else:
|
||||||
|
outputs['local'] = {'id': 'local', 'step': 0, 'data': f'{human_time()} 建立连接... '}
|
||||||
|
if req.deploy.extend == '2':
|
||||||
|
outputs['local'] = {'id': 'local', 'step': 0, 'data': f'{human_time()} 建立连接... '}
|
||||||
if req.deploy.extend == '2':
|
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)
|
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||||
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||||
if not h_actions:
|
if not h_actions:
|
||||||
|
|
|
@ -8,7 +8,7 @@ from libs.utils import AttrDict, human_time
|
||||||
from apps.repository.models import Repository
|
from apps.repository.models import Repository
|
||||||
from apps.app.utils import fetch_repo
|
from apps.app.utils import fetch_repo
|
||||||
from apps.config.utils import compose_configs
|
from apps.config.utils import compose_configs
|
||||||
import subprocess
|
from apps.deploy.helper import Helper
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
|
@ -16,20 +16,18 @@ import os
|
||||||
REPOS_DIR = settings.REPOS_DIR
|
REPOS_DIR = settings.REPOS_DIR
|
||||||
|
|
||||||
|
|
||||||
class SpugError(Exception):
|
def dispatch(rep: Repository, helper=None):
|
||||||
pass
|
rep.status = '1'
|
||||||
|
alone_build = helper is None
|
||||||
|
if not helper:
|
||||||
def dispatch(rep: Repository):
|
|
||||||
rds = get_redis_connection()
|
rds = get_redis_connection()
|
||||||
rds_key = f'{settings.BUILD_KEY}:{rep.spug_version}'
|
rds_key = f'{settings.BUILD_KEY}:{rep.spug_version}'
|
||||||
rep.status = '1'
|
|
||||||
rep.save()
|
|
||||||
helper = Helper(rds, rds_key)
|
helper = Helper(rds, rds_key)
|
||||||
|
rep.save()
|
||||||
try:
|
try:
|
||||||
api_token = uuid.uuid4().hex
|
api_token = uuid.uuid4().hex
|
||||||
rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
|
helper.rds.setex(api_token, 60 * 60, f'{rep.app_id},{rep.env_id}')
|
||||||
helper.send_info('local', f'完成\r\n{human_time()} 构建准备... ')
|
helper.send_info('local', f'\033[32m完成√\033[0m\r\n{human_time()} 构建准备... ')
|
||||||
env = AttrDict(
|
env = AttrDict(
|
||||||
SPUG_APP_NAME=rep.app.name,
|
SPUG_APP_NAME=rep.app.name,
|
||||||
SPUG_APP_ID=str(rep.app_id),
|
SPUG_APP_ID=str(rep.app_id),
|
||||||
|
@ -54,11 +52,11 @@ def dispatch(rep: Repository):
|
||||||
finally:
|
finally:
|
||||||
helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
|
helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
# save the build log for two weeks
|
if alone_build:
|
||||||
rds.expire(rds_key, 14 * 24 * 60 * 60)
|
helper.clear()
|
||||||
rds.close()
|
rep.save()
|
||||||
|
elif rep.status == '5':
|
||||||
rep.save()
|
rep.save()
|
||||||
return rep
|
|
||||||
|
|
||||||
|
|
||||||
def _build(rep: Repository, helper, env):
|
def _build(rep: Repository, helper, env):
|
||||||
|
@ -75,7 +73,7 @@ def _build(rep: Repository, helper, env):
|
||||||
tree_ish = extras[1]
|
tree_ish = extras[1]
|
||||||
env.update(SPUG_GIT_TAG=extras[1])
|
env.update(SPUG_GIT_TAG=extras[1])
|
||||||
fetch_repo(rep.deploy_id, extend.git_repo)
|
fetch_repo(rep.deploy_id, extend.git_repo)
|
||||||
helper.send_info('local', '完成\r\n')
|
helper.send_info('local', '\033[32m完成√\033[0m\r\n')
|
||||||
|
|
||||||
if extend.hook_pre_server:
|
if extend.hook_pre_server:
|
||||||
helper.send_step('local', 1, f'{human_time()} 检出前任务...\r\n')
|
helper.send_step('local', 1, f'{human_time()} 检出前任务...\r\n')
|
||||||
|
@ -84,7 +82,7 @@ def _build(rep: Repository, helper, env):
|
||||||
helper.send_step('local', 2, f'{human_time()} 执行检出... ')
|
helper.send_step('local', 2, f'{human_time()} 执行检出... ')
|
||||||
command = f'cd {git_dir} && git archive --prefix={rep.spug_version}/ {tree_ish} | (cd .. && tar xf -)'
|
command = f'cd {git_dir} && git archive --prefix={rep.spug_version}/ {tree_ish} | (cd .. && tar xf -)'
|
||||||
helper.local(command)
|
helper.local(command)
|
||||||
helper.send_info('local', '完成\r\n')
|
helper.send_info('local', '\033[32m完成√\033[0m\r\n')
|
||||||
|
|
||||||
if extend.hook_post_server:
|
if extend.hook_post_server:
|
||||||
helper.send_step('local', 3, f'{human_time()} 检出后任务...\r\n')
|
helper.send_step('local', 3, f'{human_time()} 检出后任务...\r\n')
|
||||||
|
@ -105,49 +103,5 @@ def _build(rep: Repository, helper, env):
|
||||||
else:
|
else:
|
||||||
contain = ' '.join(f'{rep.spug_version}/{x}' for x in files)
|
contain = ' '.join(f'{rep.spug_version}/{x}' for x in files)
|
||||||
helper.local(f'cd {REPOS_DIR} && tar zcf {tar_file} {exclude} {contain}')
|
helper.local(f'cd {REPOS_DIR} && tar zcf {tar_file} {exclude} {contain}')
|
||||||
helper.send_step('local', 5, f'完成')
|
helper.send_step('local', 5, f'\033[32m完成√\033[0m')
|
||||||
|
helper.send_step('local', 100, f'\r\n\r\n{human_time()} ** \033[32m构建成功\033[0m **')
|
||||||
|
|
||||||
class Helper:
|
|
||||||
def __init__(self, rds, key):
|
|
||||||
self.rds = rds
|
|
||||||
self.key = key
|
|
||||||
self.rds.delete(self.key)
|
|
||||||
|
|
||||||
def parse_filter_rule(self, data: str, sep='\n'):
|
|
||||||
data, files = data.strip(), []
|
|
||||||
if data:
|
|
||||||
for line in data.split(sep):
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#'):
|
|
||||||
files.append(line)
|
|
||||||
return files
|
|
||||||
|
|
||||||
def _send(self, message):
|
|
||||||
self.rds.rpush(self.key, json.dumps(message))
|
|
||||||
|
|
||||||
def send_info(self, key, message):
|
|
||||||
self._send({'key': key, 'data': message})
|
|
||||||
|
|
||||||
def send_error(self, key, message, with_break=True):
|
|
||||||
message = '\r\n' + message
|
|
||||||
self._send({'key': key, 'status': 'error', 'data': message})
|
|
||||||
if with_break:
|
|
||||||
raise SpugError
|
|
||||||
|
|
||||||
def send_step(self, key, step, data):
|
|
||||||
self._send({'key': key, 'step': step, 'data': data})
|
|
||||||
|
|
||||||
def local(self, command, env=None):
|
|
||||||
if env:
|
|
||||||
env = dict(env.items())
|
|
||||||
env.update(os.environ)
|
|
||||||
task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
||||||
while True:
|
|
||||||
message = task.stdout.readline()
|
|
||||||
if not message:
|
|
||||||
break
|
|
||||||
message = message.decode().rstrip('\r\n')
|
|
||||||
self.send_info('local', message + '\r\n')
|
|
||||||
if task.wait() != 0:
|
|
||||||
self.send_error('local', f'exit code: {task.returncode}')
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ function Ext1Console(props) {
|
||||||
terms[key] = term
|
terms[key] = term
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let {local, ...hosts} = outputs;
|
||||||
return store.tabModes[props.request.id] ? (
|
return store.tabModes[props.request.id] ? (
|
||||||
<Card
|
<Card
|
||||||
className={styles.item}
|
className={styles.item}
|
||||||
|
@ -95,7 +96,12 @@ function Ext1Console(props) {
|
||||||
<div className={styles.title}>{props.request.name}</div>
|
<div className={styles.title}>{props.request.name}</div>
|
||||||
<CloseOutlined onClick={() => store.showConsole(props.request, true)}/>
|
<CloseOutlined onClick={() => store.showConsole(props.request, true)}/>
|
||||||
</div>
|
</div>
|
||||||
{Object.values(outputs).map(item => (
|
{local && (
|
||||||
|
<Progress
|
||||||
|
percent={(local.step + 1) * 18}
|
||||||
|
status={local.step === 100 ? 'success' : outputs.local.status === 'error' ? 'exception' : 'active'}/>
|
||||||
|
)}
|
||||||
|
{Object.values(hosts).map(item => (
|
||||||
<Progress
|
<Progress
|
||||||
key={item.id}
|
key={item.id}
|
||||||
percent={(item.step + 1) * 18}
|
percent={(item.step + 1) * 18}
|
||||||
|
@ -117,11 +123,30 @@ function Ext1Console(props) {
|
||||||
</div>
|
</div>
|
||||||
]}>
|
]}>
|
||||||
<Skeleton loading={fetching} active>
|
<Skeleton loading={fetching} active>
|
||||||
|
{local && (
|
||||||
|
<Collapse defaultActiveKey={['0']} className={styles.collapse} style={{marginBottom: 24}}>
|
||||||
|
<Collapse.Panel header={(
|
||||||
|
<div className={styles.header}>
|
||||||
|
<b className={styles.title}/>
|
||||||
|
<Steps size="small" className={styles.step} current={local.step} status={local.status}>
|
||||||
|
<StepItem title="构建准备" item={local} step={0}/>
|
||||||
|
<StepItem title="检出前任务" item={local} step={1}/>
|
||||||
|
<StepItem title="执行检出" item={local} step={2}/>
|
||||||
|
<StepItem title="检出后任务" item={local} step={3}/>
|
||||||
|
<StepItem title="执行打包" item={local} step={4}/>
|
||||||
|
</Steps>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<OutView setTerm={term => handleSetTerm(term, 'local')}/>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
|
||||||
<Collapse
|
<Collapse
|
||||||
defaultActiveKey="0"
|
defaultActiveKey="0"
|
||||||
className={styles.collapse}
|
className={styles.collapse}
|
||||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||||
{Object.entries(outputs).map(([key, item], index) => (
|
{Object.entries(hosts).map(([key, item], index) => (
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key={index}
|
key={index}
|
||||||
header={
|
header={
|
||||||
|
|
Loading…
Reference in New Issue