mirror of https://github.com/openspug/spug
A 添加灰度发布功能
parent
6709feffeb
commit
9889c95a42
|
@ -0,0 +1,138 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django.conf import settings
|
||||||
|
from libs.utils import AttrDict, render_str, human_seconds_time
|
||||||
|
from apps.host.models import Host
|
||||||
|
from apps.repository.models import Repository
|
||||||
|
from apps.repository.utils import dispatch as build_repository
|
||||||
|
from apps.deploy.helper import SpugError
|
||||||
|
from concurrent import futures
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
BUILD_DIR = settings.BUILD_DIR
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
remarks='SPUG AUTO MAKE',
|
||||||
|
created_by_id=req.created_by_id
|
||||||
|
)
|
||||||
|
build_repository(rep, helper)
|
||||||
|
req.repository = rep
|
||||||
|
extras = json.loads(req.extra)
|
||||||
|
if extras[0] == 'repository':
|
||||||
|
extras = extras[1:]
|
||||||
|
if extras[0] == 'branch':
|
||||||
|
env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2])
|
||||||
|
else:
|
||||||
|
env.update(SPUG_GIT_TAG=extras[1])
|
||||||
|
if req.deploy.is_parallel:
|
||||||
|
threads, latest_exception = [], None
|
||||||
|
max_workers = max(10, os.cpu_count() * 5)
|
||||||
|
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
for h_id in helper.deploy_host_ids:
|
||||||
|
new_env = AttrDict(env.items())
|
||||||
|
t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env)
|
||||||
|
t.h_id = h_id
|
||||||
|
threads.append(t)
|
||||||
|
for t in futures.as_completed(threads):
|
||||||
|
exception = t.exception()
|
||||||
|
if exception:
|
||||||
|
helper.set_deploy_fail(t.h_id)
|
||||||
|
latest_exception = exception
|
||||||
|
if not isinstance(exception, SpugError):
|
||||||
|
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
||||||
|
else:
|
||||||
|
helper.set_deploy_success(t.h_id)
|
||||||
|
if latest_exception:
|
||||||
|
raise latest_exception
|
||||||
|
else:
|
||||||
|
host_ids = sorted(helper.deploy_host_ids, reverse=True)
|
||||||
|
while host_ids:
|
||||||
|
h_id = host_ids.pop()
|
||||||
|
new_env = AttrDict(env.items())
|
||||||
|
try:
|
||||||
|
_deploy_ext1_host(req, helper, h_id, new_env)
|
||||||
|
helper.set_deploy_success(h_id)
|
||||||
|
except Exception as e:
|
||||||
|
helper.set_deploy_fail(h_id)
|
||||||
|
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
||||||
|
for h_id in host_ids:
|
||||||
|
helper.set_deploy_fail(h_id)
|
||||||
|
helper.send_error(h_id, '终止发布', with_break=False)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def _deploy_ext1_host(req, helper, h_id, env):
|
||||||
|
flag = time.time()
|
||||||
|
helper.set_deploy_process(h_id)
|
||||||
|
helper.send_clear(h_id)
|
||||||
|
helper.send_info(h_id, '数据准备... ', status='doing')
|
||||||
|
host = Host.objects.filter(pk=h_id).first()
|
||||||
|
if not host:
|
||||||
|
helper.send_error(h_id, 'no such host')
|
||||||
|
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||||
|
extend = req.deploy.extend_obj
|
||||||
|
extend.dst_dir = render_str(extend.dst_dir, env)
|
||||||
|
extend.dst_repo = render_str(extend.dst_repo, env)
|
||||||
|
env.update(SPUG_DST_DIR=extend.dst_dir)
|
||||||
|
with host.get_ssh(default_env=env) as ssh:
|
||||||
|
helper.save_pid(ssh.get_pid(), h_id)
|
||||||
|
base_dst_dir = os.path.dirname(extend.dst_dir)
|
||||||
|
code, _ = ssh.exec_command_raw(
|
||||||
|
f'mkdir -p {extend.dst_repo} {base_dst_dir} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
||||||
|
if code == 0:
|
||||||
|
helper.send_error(host.id,
|
||||||
|
f'\r\n检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
||||||
|
if req.type == '2':
|
||||||
|
helper.send_warn(h_id, '跳过√\r\n')
|
||||||
|
else:
|
||||||
|
# clean
|
||||||
|
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
|
||||||
|
helper.remote_raw(host.id, ssh, f'cd {extend.dst_repo} && {clean_command}')
|
||||||
|
# transfer files
|
||||||
|
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||||
|
try:
|
||||||
|
callback = helper.progress_callback(host.id)
|
||||||
|
ssh.put_file(
|
||||||
|
os.path.join(BUILD_DIR, tar_gz_file),
|
||||||
|
os.path.join(extend.dst_repo, tar_gz_file),
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
helper.send_error(host.id, f'Exception: {e}')
|
||||||
|
|
||||||
|
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.send_success(h_id, '完成√\r\n')
|
||||||
|
|
||||||
|
# pre host
|
||||||
|
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||||
|
if extend.hook_pre_host:
|
||||||
|
helper.send_info(h_id, '发布前任务... \r\n')
|
||||||
|
command = f'cd {repo_dir} && {extend.hook_pre_host}'
|
||||||
|
helper.remote(host.id, ssh, command)
|
||||||
|
|
||||||
|
# do deploy
|
||||||
|
helper.send_info(h_id, '执行发布... ')
|
||||||
|
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
||||||
|
helper.send_success(h_id, '完成√\r\n')
|
||||||
|
|
||||||
|
# post host
|
||||||
|
if extend.hook_post_host:
|
||||||
|
helper.send_info(h_id, '发布后任务... \r\n')
|
||||||
|
command = f'cd {extend.dst_dir} && {extend.hook_post_host}'
|
||||||
|
helper.remote(host.id, ssh, command)
|
||||||
|
|
||||||
|
human_time = human_seconds_time(time.time() - flag)
|
||||||
|
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
@ -0,0 +1,170 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django.conf import settings
|
||||||
|
from libs.utils import AttrDict, render_str, human_seconds_time
|
||||||
|
from apps.host.models import Host
|
||||||
|
from apps.deploy.helper import SpugError
|
||||||
|
from concurrent import futures
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
REPOS_DIR = settings.REPOS_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def ext2_deploy(req, helper, env, with_local):
|
||||||
|
flag = time.time()
|
||||||
|
extend, step = req.deploy.extend_obj, 1
|
||||||
|
host_actions = json.loads(extend.host_actions)
|
||||||
|
server_actions = json.loads(extend.server_actions)
|
||||||
|
env.update({'SPUG_RELEASE': req.version})
|
||||||
|
if req.version:
|
||||||
|
for index, value in enumerate(req.version.split()):
|
||||||
|
env.update({f'SPUG_RELEASE_{index + 1}': value})
|
||||||
|
|
||||||
|
transfer_action = None
|
||||||
|
for action in host_actions:
|
||||||
|
if action.get('type') == 'transfer':
|
||||||
|
action['src'] = render_str(action.get('src', '').strip().rstrip('/'), env)
|
||||||
|
action['dst'] = render_str(action['dst'].strip().rstrip('/'), env)
|
||||||
|
if action.get('src_mode') == '1': # upload when publish
|
||||||
|
if not req.extra:
|
||||||
|
helper.send_error('local', '\r\n未找到上传的文件信息,请尝试新建发布申请')
|
||||||
|
extra = json.loads(req.extra)
|
||||||
|
if 'name' in extra:
|
||||||
|
action['name'] = extra['name']
|
||||||
|
else:
|
||||||
|
transfer_action = action
|
||||||
|
break
|
||||||
|
|
||||||
|
if with_local:
|
||||||
|
helper.set_deploy_process('local')
|
||||||
|
helper.send_success('local', '', status='doing')
|
||||||
|
if server_actions or transfer_action:
|
||||||
|
helper.send_clear('local')
|
||||||
|
for action in server_actions:
|
||||||
|
helper.send_info('local', f'{action["title"]}...\r\n')
|
||||||
|
helper.local(f'cd /tmp && {action["data"]}', env)
|
||||||
|
step += 1
|
||||||
|
if transfer_action:
|
||||||
|
action = transfer_action
|
||||||
|
helper.send_info('local', '检测到来源为本地路径的数据传输动作,执行打包... \r\n')
|
||||||
|
action['src'] = action['src'].rstrip('/ ')
|
||||||
|
action['dst'] = action['dst'].rstrip('/ ')
|
||||||
|
if not action['src'] or not action['dst']:
|
||||||
|
helper.send_error('local', f'Invalid path for transfer, src: {action["src"]} dst: {action["dst"]}')
|
||||||
|
if not os.path.exists(action['src']):
|
||||||
|
helper.send_error('local', f'No such file or directory: {action["src"]}')
|
||||||
|
is_dir, exclude = os.path.isdir(action['src']), ''
|
||||||
|
sp_dir, sd_dst = os.path.split(action['src'])
|
||||||
|
contain = sd_dst
|
||||||
|
if action['mode'] != '0' and is_dir:
|
||||||
|
files = helper.parse_filter_rule(action['rule'], ',', env)
|
||||||
|
if files:
|
||||||
|
if action['mode'] == '1':
|
||||||
|
contain = ' '.join(f'{sd_dst}/{x}' for x in files)
|
||||||
|
else:
|
||||||
|
excludes = []
|
||||||
|
for x in files:
|
||||||
|
if x.startswith('/'):
|
||||||
|
excludes.append(f'--exclude={sd_dst}{x}')
|
||||||
|
else:
|
||||||
|
excludes.append(f'--exclude={x}')
|
||||||
|
exclude = ' '.join(excludes)
|
||||||
|
tar_gz_file = os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, f'{req.spug_version}.tar.gz')
|
||||||
|
helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
|
||||||
|
helper.send_info('local', '打包完成\r\n')
|
||||||
|
helper.set_deploy_success('local')
|
||||||
|
human_time = human_seconds_time(time.time() - flag)
|
||||||
|
helper.send_success('local', f'\r\n** 执行完成,耗时:{human_time} **', status='success')
|
||||||
|
|
||||||
|
if host_actions:
|
||||||
|
if req.deploy.is_parallel:
|
||||||
|
threads, latest_exception = [], None
|
||||||
|
max_workers = max(10, os.cpu_count() * 5)
|
||||||
|
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
for h_id in sorted(helper.deploy_host_ids, reverse=True):
|
||||||
|
new_env = AttrDict(env.items())
|
||||||
|
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, new_env, req.spug_version)
|
||||||
|
t.h_id = h_id
|
||||||
|
threads.append(t)
|
||||||
|
for t in futures.as_completed(threads):
|
||||||
|
exception = t.exception()
|
||||||
|
if exception:
|
||||||
|
helper.set_deploy_fail(t.h_id)
|
||||||
|
latest_exception = exception
|
||||||
|
if not isinstance(exception, SpugError):
|
||||||
|
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
||||||
|
else:
|
||||||
|
helper.set_deploy_success(t.h_id)
|
||||||
|
if latest_exception:
|
||||||
|
raise latest_exception
|
||||||
|
else:
|
||||||
|
host_ids = sorted(helper.deploy_host_ids)
|
||||||
|
while host_ids:
|
||||||
|
h_id = host_ids.pop()
|
||||||
|
new_env = AttrDict(env.items())
|
||||||
|
try:
|
||||||
|
_deploy_ext2_host(helper, h_id, host_actions, new_env, req.spug_version)
|
||||||
|
helper.set_deploy_success(h_id)
|
||||||
|
except Exception as e:
|
||||||
|
helper.set_deploy_fail(h_id)
|
||||||
|
if not isinstance(e, SpugError):
|
||||||
|
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
||||||
|
for h_id in host_ids:
|
||||||
|
helper.set_deploy_fail(h_id)
|
||||||
|
helper.send_clear(h_id)
|
||||||
|
helper.send_error(h_id, '串行模式,终止发布', with_break=False)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||||
|
flag = time.time()
|
||||||
|
helper.set_deploy_process(h_id)
|
||||||
|
host = Host.objects.filter(pk=h_id).first()
|
||||||
|
if not host:
|
||||||
|
helper.send_error(h_id, 'no such host')
|
||||||
|
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||||
|
with host.get_ssh(default_env=env) as ssh:
|
||||||
|
helper.send_clear(h_id)
|
||||||
|
helper.save_pid(ssh.get_pid(), h_id)
|
||||||
|
helper.send_success(h_id, '', status='doing')
|
||||||
|
for index, action in enumerate(actions, start=1):
|
||||||
|
if action.get('type') == 'transfer':
|
||||||
|
helper.send_info(h_id, f'{action["title"]}...')
|
||||||
|
if action.get('src_mode') == '1':
|
||||||
|
try:
|
||||||
|
dst = action['dst']
|
||||||
|
command = f'[ -e {dst} ] || mkdir -p $(dirname {dst}); [ -d {dst} ]'
|
||||||
|
code, _ = ssh.exec_command_raw(command)
|
||||||
|
if code == 0: # is dir
|
||||||
|
if not action.get('name'):
|
||||||
|
raise RuntimeError('internal error 1002')
|
||||||
|
dst = dst.rstrip('/') + '/' + action['name']
|
||||||
|
callback = helper.progress_callback(host.id)
|
||||||
|
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), dst, callback)
|
||||||
|
except Exception as e:
|
||||||
|
helper.send_error(host.id, f'\r\nException: {e}')
|
||||||
|
helper.send_success(host.id, '完成√\r\n')
|
||||||
|
else:
|
||||||
|
_, sd_dst = os.path.split(action['src'])
|
||||||
|
tar_gz_file = f'{spug_version}.tar.gz'
|
||||||
|
src_file = os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, tar_gz_file)
|
||||||
|
try:
|
||||||
|
callback = helper.progress_callback(host.id)
|
||||||
|
ssh.put_file(src_file, f'/tmp/{tar_gz_file}', callback)
|
||||||
|
except Exception as e:
|
||||||
|
helper.send_error(host.id, f'\r\nException: {e}')
|
||||||
|
helper.send_success(host.id, '完成√\r\n')
|
||||||
|
command = f'mkdir -p /tmp/{spug_version} '
|
||||||
|
command += f'&& tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ 2> /dev/null '
|
||||||
|
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
|
||||||
|
command += f'&& rm -rf /tmp/{spug_version}*'
|
||||||
|
helper.remote(host.id, ssh, command)
|
||||||
|
else:
|
||||||
|
helper.send_info(h_id, f'{action["title"]}...\r\n')
|
||||||
|
command = f'cd /tmp && {action["data"]}'
|
||||||
|
helper.remote(host.id, ssh, command)
|
||||||
|
human_time = human_seconds_time(time.time() - flag)
|
||||||
|
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
@ -231,6 +231,8 @@ class Helper(NotifyMixin, KitMixin):
|
||||||
self.callback = []
|
self.callback = []
|
||||||
self.buffers = defaultdict(str)
|
self.buffers = defaultdict(str)
|
||||||
self.flags = defaultdict(bool)
|
self.flags = defaultdict(bool)
|
||||||
|
self.deploy_status = {}
|
||||||
|
self.deploy_host_ids = []
|
||||||
self.files = {}
|
self.files = {}
|
||||||
self.already_clear = False
|
self.already_clear = False
|
||||||
|
|
||||||
|
@ -242,6 +244,9 @@ class Helper(NotifyMixin, KitMixin):
|
||||||
rds.delete(rds_key)
|
rds.delete(rds_key)
|
||||||
instance = cls(rds, rds_key)
|
instance = cls(rds, rds_key)
|
||||||
for key in keys:
|
for key in keys:
|
||||||
|
if key != 'local':
|
||||||
|
instance.deploy_host_ids.append(key)
|
||||||
|
instance.deploy_status[key] = '0'
|
||||||
instance.get_file(key)
|
instance.get_file(key)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@ -282,6 +287,15 @@ class Helper(NotifyMixin, KitMixin):
|
||||||
line = f.readline()
|
line = f.readline()
|
||||||
return counter
|
return counter
|
||||||
|
|
||||||
|
def set_deploy_process(self, key):
|
||||||
|
self.deploy_status[key] = '1'
|
||||||
|
|
||||||
|
def set_deploy_success(self, key):
|
||||||
|
self.deploy_status[key] = '2'
|
||||||
|
|
||||||
|
def set_deploy_fail(self, key):
|
||||||
|
self.deploy_status[key] = '3'
|
||||||
|
|
||||||
def get_file(self, key):
|
def get_file(self, key):
|
||||||
if key in self.files:
|
if key in self.files:
|
||||||
return self.files[key]
|
return self.files[key]
|
||||||
|
|
|
@ -19,6 +19,7 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
('1', '待发布'),
|
('1', '待发布'),
|
||||||
('2', '发布中'),
|
('2', '发布中'),
|
||||||
('3', '发布成功'),
|
('3', '发布成功'),
|
||||||
|
('4', '灰度成功'),
|
||||||
)
|
)
|
||||||
TYPES = (
|
TYPES = (
|
||||||
('1', '正常发布'),
|
('1', '正常发布'),
|
||||||
|
@ -37,8 +38,7 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
version = models.CharField(max_length=100, null=True)
|
version = models.CharField(max_length=100, 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)
|
plan = models.DateTimeField(null=True)
|
||||||
fail_host_ids = models.TextField(default='[]')
|
deploy_status = models.TextField(default='{}')
|
||||||
|
|
||||||
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='+')
|
||||||
approve_at = models.CharField(max_length=20, null=True)
|
approve_at = models.CharField(max_length=20, null=True)
|
||||||
|
@ -67,6 +67,7 @@ class DeployRequest(models.Model, ModelMixin):
|
||||||
os.remove(os.path.join(settings.REPOS_DIR, str(self.deploy_id), self.spug_version))
|
os.remove(os.path.join(settings.REPOS_DIR, str(self.deploy_id), self.spug_version))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
#TODO: 清理日志文件, 删除自定义发布tar.gz文件
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<DeployRequest name={self.name}>'
|
return f'<DeployRequest name={self.name}>'
|
||||||
|
|
|
@ -4,33 +4,22 @@
|
||||||
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 django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from libs.utils import AttrDict, render_str, human_seconds_time
|
from libs.utils import AttrDict
|
||||||
from apps.host.models import Host
|
|
||||||
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.models import DeployRequest
|
from apps.deploy.models import DeployRequest
|
||||||
from apps.deploy.helper import Helper, SpugError
|
from apps.deploy.helper import Helper, SpugError
|
||||||
from concurrent import futures
|
from apps.deploy.ext1 import ext1_deploy
|
||||||
from functools import partial
|
from apps.deploy.ext2 import ext2_deploy
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
|
||||||
import os
|
|
||||||
|
|
||||||
REPOS_DIR = settings.REPOS_DIR
|
REPOS_DIR = settings.REPOS_DIR
|
||||||
BUILD_DIR = settings.BUILD_DIR
|
|
||||||
|
|
||||||
|
|
||||||
def dispatch(req, fail_mode=False):
|
def dispatch(req, deploy_host_ids, with_local):
|
||||||
rds = get_redis_connection()
|
rds = get_redis_connection()
|
||||||
rds_key = req.deploy_key
|
rds_key = req.deploy_key
|
||||||
if fail_mode:
|
keys = deploy_host_ids + ['local'] if with_local else deploy_host_ids
|
||||||
req.host_ids = req.fail_host_ids
|
|
||||||
req.fail_mode = fail_mode
|
|
||||||
req.host_ids = json.loads(req.host_ids)
|
|
||||||
req.fail_host_ids = req.host_ids[:]
|
|
||||||
keys = req.host_ids if fail_mode else req.host_ids + ['local']
|
|
||||||
helper = Helper.make(rds, rds_key, keys)
|
helper = Helper.make(rds, rds_key, keys)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -57,9 +46,9 @@ def dispatch(req, fail_mode=False):
|
||||||
env.update(configs_env)
|
env.update(configs_env)
|
||||||
|
|
||||||
if req.deploy.extend == '1':
|
if req.deploy.extend == '1':
|
||||||
_ext1_deploy(req, helper, env)
|
ext1_deploy(req, helper, env)
|
||||||
else:
|
else:
|
||||||
_ext2_deploy(req, helper, env)
|
ext2_deploy(req, helper, env, with_local)
|
||||||
req.status = '3'
|
req.status = '3'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
req.status = '-3'
|
req.status = '-3'
|
||||||
|
@ -67,283 +56,19 @@ def dispatch(req, fail_mode=False):
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
DeployRequest.objects.filter(pk=req.id).update(
|
request = DeployRequest.objects.get(pk=req.id)
|
||||||
status=req.status,
|
deploy_status = json.loads(request.deploy_status)
|
||||||
repository=req.repository,
|
deploy_status.update({str(k): v for k, v in helper.deploy_status.items()})
|
||||||
fail_host_ids=json.dumps(req.fail_host_ids),
|
values = [v for k, v in deploy_status.items() if k != 'local']
|
||||||
)
|
if all([x == '2' for x in values]):
|
||||||
|
if len(values) == len(json.loads(request.host_ids)):
|
||||||
|
request.status = '3'
|
||||||
|
else:
|
||||||
|
request.status = '4'
|
||||||
|
else:
|
||||||
|
request.status = '-3'
|
||||||
|
request.repository = req.repository
|
||||||
|
request.deploy_status = json.dumps(deploy_status)
|
||||||
|
request.save()
|
||||||
helper.clear()
|
helper.clear()
|
||||||
Helper.send_deploy_notify(req)
|
Helper.send_deploy_notify(req)
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
remarks='SPUG AUTO MAKE',
|
|
||||||
created_by_id=req.created_by_id
|
|
||||||
)
|
|
||||||
build_repository(rep, helper)
|
|
||||||
req.repository = rep
|
|
||||||
extras = json.loads(req.extra)
|
|
||||||
if extras[0] == 'repository':
|
|
||||||
extras = extras[1:]
|
|
||||||
if extras[0] == 'branch':
|
|
||||||
env.update(SPUG_GIT_BRANCH=extras[1], SPUG_GIT_COMMIT_ID=extras[2])
|
|
||||||
else:
|
|
||||||
env.update(SPUG_GIT_TAG=extras[1])
|
|
||||||
if req.deploy.is_parallel:
|
|
||||||
threads, latest_exception = [], None
|
|
||||||
max_workers = max(10, os.cpu_count() * 5)
|
|
||||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
||||||
for h_id in req.host_ids:
|
|
||||||
new_env = AttrDict(env.items())
|
|
||||||
t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env)
|
|
||||||
t.h_id = h_id
|
|
||||||
threads.append(t)
|
|
||||||
for t in futures.as_completed(threads):
|
|
||||||
exception = t.exception()
|
|
||||||
if exception:
|
|
||||||
latest_exception = exception
|
|
||||||
if not isinstance(exception, SpugError):
|
|
||||||
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
|
||||||
else:
|
|
||||||
req.fail_host_ids.remove(t.h_id)
|
|
||||||
if latest_exception:
|
|
||||||
raise latest_exception
|
|
||||||
else:
|
|
||||||
host_ids = sorted(req.host_ids, reverse=True)
|
|
||||||
while host_ids:
|
|
||||||
h_id = host_ids.pop()
|
|
||||||
new_env = AttrDict(env.items())
|
|
||||||
try:
|
|
||||||
_deploy_ext1_host(req, helper, h_id, new_env)
|
|
||||||
req.fail_host_ids.remove(h_id)
|
|
||||||
except Exception as e:
|
|
||||||
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
|
||||||
for h_id in host_ids:
|
|
||||||
helper.send_error(h_id, '终止发布', with_break=False)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def _ext2_deploy(req, helper, env):
|
|
||||||
flag = time.time()
|
|
||||||
extend, step = req.deploy.extend_obj, 1
|
|
||||||
host_actions = json.loads(extend.host_actions)
|
|
||||||
server_actions = json.loads(extend.server_actions)
|
|
||||||
env.update({'SPUG_RELEASE': req.version})
|
|
||||||
if req.version:
|
|
||||||
for index, value in enumerate(req.version.split()):
|
|
||||||
env.update({f'SPUG_RELEASE_{index + 1}': value})
|
|
||||||
|
|
||||||
transfer_action = None
|
|
||||||
for action in host_actions:
|
|
||||||
if action.get('type') == 'transfer':
|
|
||||||
action['src'] = render_str(action.get('src', '').strip().rstrip('/'), env)
|
|
||||||
action['dst'] = render_str(action['dst'].strip().rstrip('/'), env)
|
|
||||||
if action.get('src_mode') == '1': # upload when publish
|
|
||||||
if not req.extra:
|
|
||||||
helper.send_error('local', '\r\n未找到上传的文件信息,请尝试新建发布申请')
|
|
||||||
extra = json.loads(req.extra)
|
|
||||||
if 'name' in extra:
|
|
||||||
action['name'] = extra['name']
|
|
||||||
else:
|
|
||||||
transfer_action = action
|
|
||||||
break
|
|
||||||
|
|
||||||
if not req.fail_mode:
|
|
||||||
helper.send_success('local', '', status='doing')
|
|
||||||
if server_actions or transfer_action:
|
|
||||||
helper.send_clear('local')
|
|
||||||
for action in server_actions:
|
|
||||||
helper.send_info('local', f'{action["title"]}...\r\n')
|
|
||||||
helper.local(f'cd /tmp && {action["data"]}', env)
|
|
||||||
step += 1
|
|
||||||
if transfer_action:
|
|
||||||
action = transfer_action
|
|
||||||
helper.send_info('local', '检测到来源为本地路径的数据传输动作,执行打包... \r\n')
|
|
||||||
action['src'] = action['src'].rstrip('/ ')
|
|
||||||
action['dst'] = action['dst'].rstrip('/ ')
|
|
||||||
if not action['src'] or not action['dst']:
|
|
||||||
helper.send_error('local', f'Invalid path for transfer, src: {action["src"]} dst: {action["dst"]}')
|
|
||||||
if not os.path.exists(action['src']):
|
|
||||||
helper.send_error('local', f'No such file or directory: {action["src"]}')
|
|
||||||
is_dir, exclude = os.path.isdir(action['src']), ''
|
|
||||||
sp_dir, sd_dst = os.path.split(action['src'])
|
|
||||||
contain = sd_dst
|
|
||||||
if action['mode'] != '0' and is_dir:
|
|
||||||
files = helper.parse_filter_rule(action['rule'], ',', env)
|
|
||||||
if files:
|
|
||||||
if action['mode'] == '1':
|
|
||||||
contain = ' '.join(f'{sd_dst}/{x}' for x in files)
|
|
||||||
else:
|
|
||||||
excludes = []
|
|
||||||
for x in files:
|
|
||||||
if x.startswith('/'):
|
|
||||||
excludes.append(f'--exclude={sd_dst}{x}')
|
|
||||||
else:
|
|
||||||
excludes.append(f'--exclude={x}')
|
|
||||||
exclude = ' '.join(excludes)
|
|
||||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
|
||||||
helper.local(f'cd {sp_dir} && tar -zcf {tar_gz_file} {exclude} {contain}')
|
|
||||||
helper.send_info('local', '打包完成\r\n')
|
|
||||||
helper.add_callback(partial(os.remove, os.path.join(sp_dir, tar_gz_file)))
|
|
||||||
|
|
||||||
human_time = human_seconds_time(time.time() - flag)
|
|
||||||
if host_actions:
|
|
||||||
helper.send_success('local', f'\r\n** 执行完成,耗时:{human_time} **', status='success')
|
|
||||||
if req.deploy.is_parallel:
|
|
||||||
threads, latest_exception = [], None
|
|
||||||
max_workers = max(10, os.cpu_count() * 5)
|
|
||||||
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
||||||
for h_id in sorted(req.host_ids, reverse=True):
|
|
||||||
new_env = AttrDict(env.items())
|
|
||||||
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, new_env, req.spug_version)
|
|
||||||
t.h_id = h_id
|
|
||||||
threads.append(t)
|
|
||||||
for t in futures.as_completed(threads):
|
|
||||||
exception = t.exception()
|
|
||||||
if exception:
|
|
||||||
latest_exception = exception
|
|
||||||
if not isinstance(exception, SpugError):
|
|
||||||
helper.send_error(t.h_id, f'Exception: {exception}', with_break=False)
|
|
||||||
else:
|
|
||||||
req.fail_host_ids.remove(t.h_id)
|
|
||||||
if latest_exception:
|
|
||||||
raise latest_exception
|
|
||||||
else:
|
|
||||||
host_ids = sorted(req.host_ids)
|
|
||||||
while host_ids:
|
|
||||||
h_id = host_ids.pop()
|
|
||||||
new_env = AttrDict(env.items())
|
|
||||||
try:
|
|
||||||
_deploy_ext2_host(helper, h_id, host_actions, new_env, req.spug_version)
|
|
||||||
req.fail_host_ids.remove(h_id)
|
|
||||||
except Exception as e:
|
|
||||||
if not isinstance(e, SpugError):
|
|
||||||
helper.send_error(h_id, f'Exception: {e}', with_break=False)
|
|
||||||
for h_id in host_ids:
|
|
||||||
helper.send_clear(h_id)
|
|
||||||
helper.send_error(h_id, '串行模式,终止发布', with_break=False)
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
req.fail_host_ids = []
|
|
||||||
helper.send_success('local', f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
||||||
|
|
||||||
|
|
||||||
def _deploy_ext1_host(req, helper, h_id, env):
|
|
||||||
flag = time.time()
|
|
||||||
helper.send_clear(h_id)
|
|
||||||
helper.send_info(h_id, '数据准备... ', status='doing')
|
|
||||||
host = Host.objects.filter(pk=h_id).first()
|
|
||||||
if not host:
|
|
||||||
helper.send_error(h_id, 'no such host')
|
|
||||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
|
||||||
extend = req.deploy.extend_obj
|
|
||||||
extend.dst_dir = render_str(extend.dst_dir, env)
|
|
||||||
extend.dst_repo = render_str(extend.dst_repo, env)
|
|
||||||
env.update(SPUG_DST_DIR=extend.dst_dir)
|
|
||||||
with host.get_ssh(default_env=env) as ssh:
|
|
||||||
helper.save_pid(ssh.get_pid(), h_id)
|
|
||||||
base_dst_dir = os.path.dirname(extend.dst_dir)
|
|
||||||
code, _ = ssh.exec_command_raw(
|
|
||||||
f'mkdir -p {extend.dst_repo} {base_dst_dir} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
|
||||||
if code == 0:
|
|
||||||
helper.send_error(host.id,
|
|
||||||
f'\r\n检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
|
||||||
if req.type == '2':
|
|
||||||
helper.send_warn(h_id, '跳过√\r\n')
|
|
||||||
else:
|
|
||||||
# clean
|
|
||||||
clean_command = f'ls -d {extend.deploy_id}_* 2> /dev/null | sort -t _ -rnk2 | tail -n +{extend.versions + 1} | xargs rm -rf'
|
|
||||||
helper.remote_raw(host.id, ssh, f'cd {extend.dst_repo} && {clean_command}')
|
|
||||||
# transfer files
|
|
||||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
|
||||||
try:
|
|
||||||
callback = helper.progress_callback(host.id)
|
|
||||||
ssh.put_file(
|
|
||||||
os.path.join(BUILD_DIR, tar_gz_file),
|
|
||||||
os.path.join(extend.dst_repo, tar_gz_file),
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
helper.send_error(host.id, f'Exception: {e}')
|
|
||||||
|
|
||||||
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.send_success(h_id, '完成√\r\n')
|
|
||||||
|
|
||||||
# pre host
|
|
||||||
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
|
||||||
if extend.hook_pre_host:
|
|
||||||
helper.send_info(h_id, '发布前任务... \r\n')
|
|
||||||
command = f'cd {repo_dir} && {extend.hook_pre_host}'
|
|
||||||
helper.remote(host.id, ssh, command)
|
|
||||||
|
|
||||||
# do deploy
|
|
||||||
helper.send_info(h_id, '执行发布... ')
|
|
||||||
helper.remote_raw(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
|
||||||
helper.send_success(h_id, '完成√\r\n')
|
|
||||||
|
|
||||||
# post host
|
|
||||||
if extend.hook_post_host:
|
|
||||||
helper.send_info(h_id, '发布后任务... \r\n')
|
|
||||||
command = f'cd {extend.dst_dir} && {extend.hook_post_host}'
|
|
||||||
helper.remote(host.id, ssh, command)
|
|
||||||
|
|
||||||
human_time = human_seconds_time(time.time() - flag)
|
|
||||||
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
||||||
|
|
||||||
|
|
||||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
|
||||||
flag = time.time()
|
|
||||||
host = Host.objects.filter(pk=h_id).first()
|
|
||||||
if not host:
|
|
||||||
helper.send_error(h_id, 'no such host')
|
|
||||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
|
||||||
with host.get_ssh(default_env=env) as ssh:
|
|
||||||
helper.send_clear(h_id)
|
|
||||||
helper.save_pid(ssh.get_pid(), h_id)
|
|
||||||
helper.send_success(h_id, '', status='doing')
|
|
||||||
for index, action in enumerate(actions, start=1):
|
|
||||||
if action.get('type') == 'transfer':
|
|
||||||
helper.send_info(h_id, f'{action["title"]}...')
|
|
||||||
if action.get('src_mode') == '1':
|
|
||||||
try:
|
|
||||||
dst = action['dst']
|
|
||||||
command = f'[ -e {dst} ] || mkdir -p $(dirname {dst}); [ -d {dst} ]'
|
|
||||||
code, _ = ssh.exec_command_raw(command)
|
|
||||||
if code == 0: # is dir
|
|
||||||
if not action.get('name'):
|
|
||||||
raise RuntimeError('internal error 1002')
|
|
||||||
dst = dst.rstrip('/') + '/' + action['name']
|
|
||||||
callback = helper.progress_callback(host.id)
|
|
||||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), dst, callback)
|
|
||||||
except Exception as e:
|
|
||||||
helper.send_error(host.id, f'\r\nException: {e}')
|
|
||||||
helper.send_success(host.id, '完成√\r\n')
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
sp_dir, sd_dst = os.path.split(action['src'])
|
|
||||||
tar_gz_file = f'{spug_version}.tar.gz'
|
|
||||||
try:
|
|
||||||
callback = helper.progress_callback(host.id)
|
|
||||||
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}', callback)
|
|
||||||
except Exception as e:
|
|
||||||
helper.send_error(host.id, f'\r\nException: {e}')
|
|
||||||
|
|
||||||
command = f'mkdir -p /tmp/{spug_version} '
|
|
||||||
command += f'&& tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ 2> /dev/null '
|
|
||||||
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
|
|
||||||
command += f'&& rm -rf /tmp/{spug_version}* && echo "\033[32m完成√\033[0m"'
|
|
||||||
else:
|
|
||||||
helper.send_info(h_id, f'{action["title"]}...\r\n')
|
|
||||||
command = f'cd /tmp && {action["data"]}'
|
|
||||||
helper.remote(host.id, ssh, command)
|
|
||||||
human_time = human_seconds_time(time.time() - flag)
|
|
||||||
helper.send_success(h_id, f'\r\n** 发布成功,耗时:{human_time} **', status='success')
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ 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.conf import settings
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django_redis import get_redis_connection
|
|
||||||
from libs import json_response, JsonParser, Argument, human_datetime, human_time, auth, AttrDict
|
from libs import json_response, JsonParser, Argument, human_datetime, human_time, auth, AttrDict
|
||||||
from apps.deploy.models import DeployRequest
|
from apps.deploy.models import DeployRequest
|
||||||
from apps.app.models import Deploy, DeployExtend2
|
from apps.app.models import Deploy, DeployExtend2
|
||||||
|
@ -46,7 +45,7 @@ class RequestView(View):
|
||||||
tmp['app_name'] = item.app_name
|
tmp['app_name'] = item.app_name
|
||||||
tmp['app_extend'] = item.app_extend
|
tmp['app_extend'] = item.app_extend
|
||||||
tmp['host_ids'] = json.loads(item.host_ids)
|
tmp['host_ids'] = json.loads(item.host_ids)
|
||||||
tmp['fail_host_ids'] = json.loads(item.fail_host_ids)
|
tmp['deploy_status'] = json.loads(item.deploy_status)
|
||||||
tmp['extra'] = json.loads(item.extra) if item.extra else None
|
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['rep_extra'] = json.loads(item.rep_extra) if item.rep_extra else None
|
||||||
tmp['app_host_ids'] = json.loads(item.app_host_ids)
|
tmp['app_host_ids'] = json.loads(item.app_host_ids)
|
||||||
|
@ -125,49 +124,72 @@ class RequestDetailView(View):
|
||||||
|
|
||||||
@auth('deploy.request.do')
|
@auth('deploy.request.do')
|
||||||
def post(self, request, r_id):
|
def post(self, request, r_id):
|
||||||
form, _ = JsonParser(Argument('mode', default='all')).parse(request.body)
|
form, error = JsonParser(
|
||||||
query, is_fail_mode = {'pk': r_id}, form.mode == 'fail'
|
Argument('mode', filter=lambda x: x in ('fail', 'gray', 'all'), help='参数错误'),
|
||||||
if not request.user.is_supper:
|
Argument('host_ids', type=list, required=False)
|
||||||
perms = request.user.deploy_perms
|
).parse(request.body)
|
||||||
query['deploy__app_id__in'] = perms['apps']
|
if error is None:
|
||||||
query['deploy__env_id__in'] = perms['envs']
|
query, is_fail_mode = {'pk': r_id}, form.mode == 'fail'
|
||||||
req = DeployRequest.objects.filter(**query).first()
|
if not request.user.is_supper:
|
||||||
if not req:
|
perms = request.user.deploy_perms
|
||||||
return json_response(error='未找到指定发布申请')
|
query['deploy__app_id__in'] = perms['apps']
|
||||||
if req.status not in ('1', '-3'):
|
query['deploy__env_id__in'] = perms['envs']
|
||||||
return json_response(error='该申请单当前状态还不能执行发布')
|
req = DeployRequest.objects.filter(**query).first()
|
||||||
host_ids = req.fail_host_ids if is_fail_mode else req.host_ids
|
if not req:
|
||||||
|
return json_response(error='未找到指定发布申请')
|
||||||
|
if req.status not in ('1', '-3', '4'):
|
||||||
|
return json_response(error='该申请单当前状态还不能执行发布')
|
||||||
|
|
||||||
req.status = '2'
|
deploy_status = json.loads(req.deploy_status)
|
||||||
req.do_at = human_datetime()
|
if form.mode == 'gray':
|
||||||
req.do_by = request.user
|
if not form.host_ids:
|
||||||
req.save()
|
return json_response(error='请选择灰度发布的主机')
|
||||||
Thread(target=dispatch, args=(req, is_fail_mode)).start()
|
host_ids = form.host_ids
|
||||||
|
elif form.mode == 'fail':
|
||||||
hosts = Host.objects.filter(id__in=json.loads(host_ids))
|
host_ids = [int(k) for k, v in deploy_status.items() if v != '2' and k != 'local']
|
||||||
message = Helper.term_message('等待调度... ')
|
|
||||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': message} for x in hosts}
|
|
||||||
if req.is_quick_deploy:
|
|
||||||
if req.repository_id:
|
|
||||||
outputs['local'] = {
|
|
||||||
'id': 'local',
|
|
||||||
'status': 'success',
|
|
||||||
'data': Helper.term_message('已构建完成忽略执行', 'warn')
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')}
|
host_ids = json.loads(req.host_ids)
|
||||||
if req.deploy.extend == '2':
|
|
||||||
message = Helper.term_message('等待初始化... ')
|
with_local = False
|
||||||
if is_fail_mode:
|
hosts = Host.objects.filter(id__in=host_ids)
|
||||||
message = Helper.term_message('已完成本地动作忽略执行', 'warn')
|
message = Helper.term_message('等待调度... ')
|
||||||
outputs['local'] = {'id': 'local', 'data': message}
|
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': message} for x in hosts}
|
||||||
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
if req.deploy.extend == '1':
|
||||||
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
if req.repository_id:
|
||||||
if not s_actions:
|
if req.is_quick_deploy:
|
||||||
outputs.pop('local')
|
outputs['local'] = {
|
||||||
if not h_actions:
|
'id': 'local',
|
||||||
outputs = {'local': outputs['local']}
|
'status': 'success',
|
||||||
return json_response({'outputs': outputs, 'token': req.deploy_key})
|
'data': Helper.term_message('已构建完成忽略执行', 'warn')
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
with_local = True
|
||||||
|
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')}
|
||||||
|
elif req.deploy.extend == '2':
|
||||||
|
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||||
|
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||||
|
if s_actions:
|
||||||
|
if deploy_status.get('local') == '2':
|
||||||
|
outputs['local'] = {
|
||||||
|
'id': 'local',
|
||||||
|
'status': 'success',
|
||||||
|
'data': Helper.term_message('已完成本地动作忽略执行', 'warn')
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
with_local = True
|
||||||
|
outputs['local'] = {'id': 'local', 'data': Helper.term_message('等待初始化... ')}
|
||||||
|
if not h_actions:
|
||||||
|
outputs = {'local': outputs['local']}
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
req.status = '2'
|
||||||
|
req.do_at = human_datetime()
|
||||||
|
req.do_by = request.user
|
||||||
|
req.save()
|
||||||
|
Thread(target=dispatch, args=(req, host_ids, with_local)).start()
|
||||||
|
return json_response({'outputs': outputs, 'token': req.deploy_key})
|
||||||
|
return json_response(error=error)
|
||||||
|
|
||||||
@auth('deploy.request.approve')
|
@auth('deploy.request.approve')
|
||||||
def patch(self, request, r_id):
|
def patch(self, request, r_id):
|
||||||
|
@ -323,7 +345,7 @@ def get_request_info(request):
|
||||||
if error is None:
|
if error is None:
|
||||||
req = DeployRequest.objects.get(pk=form.id)
|
req = DeployRequest.objects.get(pk=form.id)
|
||||||
response = req.to_dict(selects=('status', 'reason'))
|
response = req.to_dict(selects=('status', 'reason'))
|
||||||
response['fail_host_ids'] = json.loads(req.fail_host_ids)
|
response['deploy_status'] = json.loads(req.deploy_status)
|
||||||
response['status_alias'] = req.get_status_display()
|
response['status_alias'] = req.get_status_display()
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
return json_response(error=error)
|
return json_response(error=error)
|
||||||
|
|
|
@ -26,6 +26,7 @@ def dispatch(rep: Repository, helper=None):
|
||||||
helper = Helper.make(rds, rep.deploy_key, ['local'])
|
helper = Helper.make(rds, rep.deploy_key, ['local'])
|
||||||
rep.save()
|
rep.save()
|
||||||
try:
|
try:
|
||||||
|
helper.set_deploy_process('local')
|
||||||
api_token = uuid.uuid4().hex
|
api_token = uuid.uuid4().hex
|
||||||
helper.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}')
|
||||||
env = AttrDict(
|
env = AttrDict(
|
||||||
|
@ -63,8 +64,10 @@ def dispatch(rep: Repository, helper=None):
|
||||||
|
|
||||||
_build(rep, helper, env)
|
_build(rep, helper, env)
|
||||||
rep.status = '5'
|
rep.status = '5'
|
||||||
|
helper.set_deploy_success('local')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
rep.status = '2'
|
rep.status = '2'
|
||||||
|
helper.set_deploy_fail('local')
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
|
helper.local(f'cd {REPOS_DIR} && rm -rf {rep.spug_version}')
|
||||||
|
|
|
@ -78,7 +78,12 @@ function Console(props) {
|
||||||
|
|
||||||
function doDeploy() {
|
function doDeploy() {
|
||||||
let socket;
|
let socket;
|
||||||
http.post(`/api/deploy/request/${props.request.id}/`, {mode: props.request.mode})
|
const formData = {mode: props.request.mode}
|
||||||
|
if (Array.isArray(props.request.mode)) {
|
||||||
|
formData.mode = 'gray'
|
||||||
|
formData.host_ids = props.request.mode
|
||||||
|
}
|
||||||
|
http.post(`/api/deploy/request/${props.request.id}/`, formData)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
_handleResponse(res)
|
_handleResponse(res)
|
||||||
socket = _makeSocket()
|
socket = _makeSocket()
|
||||||
|
|
|
@ -249,6 +249,7 @@ export default observer(function () {
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
{visible && <HostSelector
|
{visible && <HostSelector
|
||||||
|
title="可选主机列表"
|
||||||
host_ids={host_ids}
|
host_ids={host_ids}
|
||||||
app_host_ids={app_host_ids}
|
app_host_ids={app_host_ids}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setVisible(false)}
|
||||||
|
|
|
@ -120,6 +120,7 @@ export default observer(function () {
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
{visible && <HostSelector
|
{visible && <HostSelector
|
||||||
|
title="可选主机列表"
|
||||||
host_ids={host_ids}
|
host_ids={host_ids}
|
||||||
app_host_ids={app_host_ids}
|
app_host_ids={app_host_ids}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setVisible(false)}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Modal, Table, Button, Alert } from 'antd';
|
import { Modal, Table, Button, Alert, Tag } from 'antd';
|
||||||
import hostStore from 'pages/host/store';
|
import hostStore from 'pages/host/store';
|
||||||
|
|
||||||
export default observer(function (props) {
|
export default observer(function (props) {
|
||||||
|
@ -31,19 +31,34 @@ export default observer(function (props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DeployStatus(props) {
|
||||||
|
switch (props.status) {
|
||||||
|
case '0':
|
||||||
|
return <Tag color="blue">待调度</Tag>
|
||||||
|
case '1':
|
||||||
|
return <Tag color="orange">发布中</Tag>
|
||||||
|
case '2':
|
||||||
|
return <Tag color="green">发布成功</Tag>
|
||||||
|
case '3':
|
||||||
|
return <Tag color="red">发布失败</Tag>
|
||||||
|
default:
|
||||||
|
return <Tag color="blue">待发布</Tag>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
width={600}
|
width={800}
|
||||||
title='可选主机列表'
|
title={props.title}
|
||||||
onOk={handleSubmit}
|
onOk={handleSubmit}
|
||||||
|
okButtonProps={{disabled: selectedRowKeys.length === 0}}
|
||||||
onCancel={props.onCancel}>
|
onCancel={props.onCancel}>
|
||||||
{selectedRowKeys.length > 0 && (
|
<Alert
|
||||||
<Alert
|
style={{marginBottom: 12}}
|
||||||
style={{marginBottom: 12}}
|
message={<span>已选择 <b style={{color: '#2563fc', fontSize: 18}}>{selectedRowKeys.length}</b> 台主机</span>}
|
||||||
message={`已选择 ${selectedRowKeys.length} 台主机`}
|
action={<Button type="link" disabled={selectedRowKeys.length === 0}
|
||||||
action={<Button type="link" onClick={() => setSelectedRowKeys([])}>取消选择</Button>}/>
|
onClick={() => setSelectedRowKeys([])}>取消选择</Button>}/>
|
||||||
)}
|
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={hostStore.records.filter(x => props.app_host_ids.includes(x.id))}
|
dataSource={hostStore.records.filter(x => props.app_host_ids.includes(x.id))}
|
||||||
|
@ -61,6 +76,10 @@ export default observer(function (props) {
|
||||||
}}>
|
}}>
|
||||||
<Table.Column title="主机名称" dataIndex="name"/>
|
<Table.Column title="主机名称" dataIndex="name"/>
|
||||||
<Table.Column title="连接地址" dataIndex="hostname"/>
|
<Table.Column title="连接地址" dataIndex="hostname"/>
|
||||||
|
<Table.Column title="备注信息" dataIndex="desc"/>
|
||||||
|
{props.deploy_status ? (
|
||||||
|
<Table.Column title="发布状态" render={v => <DeployStatus status={props.deploy_status[v.id]}/>}/>
|
||||||
|
) : null}
|
||||||
</Table>
|
</Table>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default observer(function () {
|
||||||
}, () => setLoading(false))
|
}, () => setLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
const {app_host_ids, deploy_id} = store.record;
|
const {app_host_ids, deploy_id, deploy_status} = store.record;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible
|
visible
|
||||||
|
@ -79,8 +79,10 @@ export default observer(function () {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
{visible && <HostSelector
|
{visible && <HostSelector
|
||||||
|
title="选择回滚发布的主机"
|
||||||
host_ids={host_ids}
|
host_ids={host_ids}
|
||||||
app_host_ids={app_host_ids}
|
app_host_ids={app_host_ids}
|
||||||
|
deploy_status={deploy_status}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setVisible(false)}
|
||||||
onOk={ids => setHostIds(ids)}/>}
|
onOk={ids => setHostIds(ids)}/>}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -3,27 +3,21 @@
|
||||||
* Copyright (c) <spug.dev@gmail.com>
|
* Copyright (c) <spug.dev@gmail.com>
|
||||||
* Released under the AGPL-3.0 License.
|
* Released under the AGPL-3.0 License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined, TagsOutlined } from '@ant-design/icons';
|
import { BranchesOutlined, BuildOutlined, TagOutlined, PlusOutlined, TagsOutlined } from '@ant-design/icons';
|
||||||
import { Radio, Modal, Popover, Tag, Popconfirm, Tooltip, message } from 'antd';
|
import { Radio, Modal, Popover, Tag, Tooltip, Button, Space, 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 HostSelector from './HostSelector';
|
||||||
import S from './index.module.less';
|
import S from './index.module.less';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import lds from 'lodash';
|
||||||
function DeployConfirm() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>确认发布方式</div>
|
|
||||||
<div style={{color: '#999', fontSize: 12}}>补偿:仅发布上次发布失败的主机。</div>
|
|
||||||
<div style={{color: '#999', fontSize: 12}}>全量:再次发布所有主机。</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComTable() {
|
function ComTable() {
|
||||||
|
const [request, setRequest] = useState()
|
||||||
|
|
||||||
const columns = [{
|
const columns = [{
|
||||||
title: '申请标题',
|
title: '申请标题',
|
||||||
className: S.min180,
|
className: S.min180,
|
||||||
|
@ -144,6 +138,14 @@ function ComTable() {
|
||||||
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
|
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
|
||||||
)}
|
)}
|
||||||
</Action>;
|
</Action>;
|
||||||
|
case '4':
|
||||||
|
return <Action>
|
||||||
|
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
|
||||||
|
<DoAction info={info}/>
|
||||||
|
{info.visible_rollback && (
|
||||||
|
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
|
||||||
|
)}
|
||||||
|
</Action>;
|
||||||
case '-1':
|
case '-1':
|
||||||
return <Action>
|
return <Action>
|
||||||
<Action.Button auth="deploy.request.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
|
<Action.Button auth="deploy.request.edit" onClick={() => store.showForm(info)}>编辑</Action.Button>
|
||||||
|
@ -171,17 +173,23 @@ function ComTable() {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
function DoAction(props) {
|
function DoAction(props) {
|
||||||
const {host_ids, fail_host_ids} = props.info;
|
const {deploy_status} = props.info;
|
||||||
return (
|
return (
|
||||||
<Popconfirm
|
<Popover trigger="click" zIndex={2} title="确认发布方式" content={(
|
||||||
title={<DeployConfirm/>}
|
<div>
|
||||||
okText="全量"
|
<div style={{color: '#999', fontSize: 12}}>全量:发布所有主机(包含已成功的)。</div>
|
||||||
cancelText="补偿"
|
<div style={{color: '#999', fontSize: 12}}>补偿:仅发布上次发布失败的主机。</div>
|
||||||
cancelButtonProps={{disabled: [0, host_ids.length].includes(fail_host_ids.length)}}
|
<div style={{color: '#999', fontSize: 12}}>灰度:选择指定主机发布。</div>
|
||||||
onConfirm={e => handleDeploy(e, props.info, 'all')}
|
<Space style={{width: '100%', justifyContent: 'flex-end', marginTop: 16}}>
|
||||||
onCancel={e => handleDeploy(e, props.info, 'fail')}>
|
<Button size="small" disabled={!lds.findKey(deploy_status, x => x !== '2')}
|
||||||
|
onClick={() => handleDeploy(props.info, 'fail')}>补偿</Button>
|
||||||
|
<Button ghost size="small" type="primary" onClick={() => setRequest(props.info)}>灰度</Button>
|
||||||
|
<Button size="small" type="primary" onClick={() => handleDeploy(props.info, 'all')}>全量</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
<Action.Button auth="deploy.request.do">发布</Action.Button>
|
<Action.Button auth="deploy.request.do">发布</Action.Button>
|
||||||
</Popconfirm>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,43 +207,58 @@ function ComTable() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeploy(e, info, mode) {
|
function handleDeploy(info, mode) {
|
||||||
|
if (request && mode.length === 0) {
|
||||||
|
return message.error('请选择灰度发布的主机')
|
||||||
|
}
|
||||||
info.mode = mode
|
info.mode = mode
|
||||||
store.showConsole(info);
|
store.showConsole(info)
|
||||||
|
if (request) setRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCard
|
<React.Fragment>
|
||||||
tKey="dr"
|
<TableCard
|
||||||
rowKey={row => row.key || row.id}
|
tKey="dr"
|
||||||
title="申请列表"
|
rowKey={row => row.key || row.id}
|
||||||
columns={columns}
|
title="申请列表"
|
||||||
scroll={{x: 1500}}
|
columns={columns}
|
||||||
tableLayout="auto"
|
scroll={{x: 1500}}
|
||||||
loading={store.isFetching}
|
tableLayout="auto"
|
||||||
dataSource={store.dataSource}
|
loading={store.isFetching}
|
||||||
onReload={store.fetchRecords}
|
dataSource={store.dataSource}
|
||||||
actions={[
|
onReload={store.fetchRecords}
|
||||||
<AuthButton
|
actions={[
|
||||||
auth="deploy.request.add"
|
<AuthButton
|
||||||
type="primary"
|
auth="deploy.request.add"
|
||||||
icon={<PlusOutlined/>}
|
type="primary"
|
||||||
onClick={() => store.addVisible = true}>新建申请</AuthButton>,
|
icon={<PlusOutlined/>}
|
||||||
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
|
onClick={() => store.addVisible = true}>新建申请</AuthButton>,
|
||||||
<Radio.Button value="all">全部({store.counter['all'] || 0})</Radio.Button>
|
<Radio.Group value={store.f_status} onChange={e => store.f_status = e.target.value}>
|
||||||
<Radio.Button value="0">待审核({store.counter['0'] || 0})</Radio.Button>
|
<Radio.Button value="all">全部({store.counter['all'] || 0})</Radio.Button>
|
||||||
<Radio.Button value="1">待发布({store.counter['1'] || 0})</Radio.Button>
|
<Radio.Button value="0">待审核({store.counter['0'] || 0})</Radio.Button>
|
||||||
<Radio.Button value="3">发布成功({store.counter['3'] || 0})</Radio.Button>
|
<Radio.Button value="1">待发布({store.counter['1'] || 0})</Radio.Button>
|
||||||
<Radio.Button value="-3">发布异常({store.counter['-3'] || 0})</Radio.Button>
|
<Radio.Button value="3">发布成功({store.counter['3'] || 0})</Radio.Button>
|
||||||
<Radio.Button value="99">其他({store.counter['99'] || 0})</Radio.Button>
|
<Radio.Button value="-3">发布异常({store.counter['-3'] || 0})</Radio.Button>
|
||||||
</Radio.Group>
|
<Radio.Button value="99">其他({store.counter['99'] || 0})</Radio.Button>
|
||||||
]}
|
</Radio.Group>
|
||||||
pagination={{
|
]}
|
||||||
showSizeChanger: true,
|
pagination={{
|
||||||
showLessItems: true,
|
showSizeChanger: true,
|
||||||
showTotal: total => `共 ${total} 条`,
|
showLessItems: true,
|
||||||
pageSizeOptions: ['10', '20', '50', '100']
|
showTotal: total => `共 ${total} 条`,
|
||||||
}}/>
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
}}/>
|
||||||
|
|
||||||
|
{request ? (
|
||||||
|
<HostSelector
|
||||||
|
title="选择灰度发布的主机"
|
||||||
|
app_host_ids={request.host_ids}
|
||||||
|
onCancel={() => setRequest()}
|
||||||
|
deploy_status={request.deploy_status}
|
||||||
|
onOk={ids => handleDeploy(request, ids)}/>
|
||||||
|
) : null}
|
||||||
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ class Store {
|
||||||
};
|
};
|
||||||
|
|
||||||
rollback = (info) => {
|
rollback = (info) => {
|
||||||
this.record = lds.pick(info, ['deploy_id', 'host_ids']);
|
this.record = lds.pick(info, ['deploy_id', 'host_ids', 'deploy_status']);
|
||||||
this.record.app_host_ids = info.host_ids;
|
this.record.app_host_ids = info.host_ids;
|
||||||
this.record.name = `${info.name} - 回滚`;
|
this.record.name = `${info.name} - 回滚`;
|
||||||
this.rollbackVisible = true
|
this.rollbackVisible = true
|
||||||
|
|
Loading…
Reference in New Issue