A 增加补偿发布功能

pull/480/head
vapao 2022-04-13 11:14:14 +08:00
parent b96851b250
commit ca2775c9bf
10 changed files with 99 additions and 28 deletions

View File

@ -17,9 +17,28 @@ class Helper:
def __init__(self, rds, key): def __init__(self, rds, key):
self.rds = rds self.rds = rds
self.key = key self.key = key
self.rds.delete(self.key)
self.callback = [] self.callback = []
@classmethod
def make(cls, rds, key, host_ids=None):
if host_ids:
counter, tmp_key = 0, f'{key}_tmp'
data = rds.lrange(key, counter, counter + 9)
while data:
for item in data:
counter += 1
print(item)
tmp = json.loads(item.decode())
if tmp['key'] not in host_ids:
rds.rpush(tmp_key, item)
data = rds.lrange(key, counter, counter + 9)
rds.delete(key)
if rds.exists(tmp_key):
rds.rename(tmp_key, key)
else:
rds.delete(key)
return cls(rds, key)
@classmethod @classmethod
def _make_dd_notify(cls, url, action, req, version, host_str): def _make_dd_notify(cls, url, action, req, version, host_str):
texts = [ texts = [
@ -169,7 +188,7 @@ class Helper:
@classmethod @classmethod
def send_deploy_notify(cls, req, action=None): def send_deploy_notify(cls, req, action=None):
rst_notify = json.loads(req.deploy.rst_notify) rst_notify = json.loads(req.deploy.rst_notify)
host_ids = json.loads(req.host_ids) host_ids = req.host_ids
if rst_notify['mode'] != '0' and rst_notify.get('value'): if rst_notify['mode'] != '0' and rst_notify.get('value'):
url = rst_notify['value'] url = rst_notify['value']
version = req.version version = req.version
@ -232,6 +251,7 @@ class Helper:
self._send({'key': key, 'step': step, 'data': data}) self._send({'key': key, 'step': step, 'data': data})
def clear(self): def clear(self):
self.rds.delete(f'{self.key}_tmp')
# save logs for two weeks # save logs for two weeks
self.rds.expire(self.key, 14 * 24 * 60 * 60) self.rds.expire(self.key, 14 * 24 * 60 * 60)
self.rds.close() self.rds.close()

View File

@ -37,6 +37,7 @@ class DeployRequest(models.Model, ModelMixin):
version = models.CharField(max_length=50, null=True) version = models.CharField(max_length=50, 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='[]')
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='+')

View File

@ -9,6 +9,7 @@ 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.models import Repository
from apps.repository.utils import dispatch as build_repository from apps.repository.utils import dispatch as build_repository
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 concurrent import futures
from functools import partial from functools import partial
@ -19,10 +20,16 @@ import os
REPOS_DIR = settings.REPOS_DIR REPOS_DIR = settings.REPOS_DIR
def dispatch(req): def dispatch(req, fail_mode=False):
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) if fail_mode:
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[:]
helper = Helper.make(rds, rds_key, req.host_ids if fail_mode else None)
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}')
@ -56,7 +63,11 @@ def dispatch(req):
raise e raise e
finally: finally:
close_old_connections() close_old_connections()
req.save() DeployRequest.objects.filter(pk=req.id).update(
status=req.status,
repository=req.repository,
fail_host_ids=json.dumps(req.fail_host_ids),
)
helper.clear() helper.clear()
Helper.send_deploy_notify(req) Helper.send_deploy_notify(req)
@ -86,7 +97,7 @@ def _ext1_deploy(req, helper, env):
threads, latest_exception = [], None threads, latest_exception = [], None
max_workers = max(10, os.cpu_count() * 5) max_workers = max(10, os.cpu_count() * 5)
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor: with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
for h_id in json.loads(req.host_ids): for h_id in req.host_ids:
new_env = AttrDict(env.items()) new_env = AttrDict(env.items())
t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env) t = executor.submit(_deploy_ext1_host, req, helper, h_id, new_env)
t.h_id = h_id t.h_id = h_id
@ -97,15 +108,18 @@ def _ext1_deploy(req, helper, env):
latest_exception = exception latest_exception = exception
if not isinstance(exception, SpugError): if not isinstance(exception, SpugError):
helper.send_error(t.h_id, f'Exception: {exception}', False) helper.send_error(t.h_id, f'Exception: {exception}', False)
else:
req.fail_host_ids.remove(t.h_id)
if latest_exception: if latest_exception:
raise latest_exception raise latest_exception
else: else:
host_ids = sorted(json.loads(req.host_ids), reverse=True) host_ids = sorted(req.host_ids, reverse=True)
while host_ids: while host_ids:
h_id = host_ids.pop() h_id = host_ids.pop()
new_env = AttrDict(env.items()) new_env = AttrDict(env.items())
try: try:
_deploy_ext1_host(req, helper, h_id, new_env) _deploy_ext1_host(req, helper, h_id, new_env)
req.fail_host_ids.remove(h_id)
except Exception as e: except Exception as e:
helper.send_error(h_id, f'Exception: {e}', False) helper.send_error(h_id, f'Exception: {e}', False)
for h_id in host_ids: for h_id in host_ids:
@ -114,7 +128,6 @@ def _ext1_deploy(req, helper, env):
def _ext2_deploy(req, helper, env): def _ext2_deploy(req, helper, env):
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)
@ -122,10 +135,13 @@ def _ext2_deploy(req, helper, env):
if req.version: if req.version:
for index, value in enumerate(req.version.split()): for index, value in enumerate(req.version.split()):
env.update({f'SPUG_RELEASE_{index + 1}': value}) env.update({f'SPUG_RELEASE_{index + 1}': value})
for action in server_actions:
helper.send_step('local', step, f'{human_time()} {action["title"]}...\r\n') if not req.fail_mode:
helper.local(f'cd /tmp && {action["data"]}', env) helper.send_info('local', f'\033[32m完成√\033[0m\r\n')
step += 1 for action in server_actions:
helper.send_step('local', step, f'{human_time()} {action["title"]}...\r\n')
helper.local(f'cd /tmp && {action["data"]}', env)
step += 1
for action in host_actions: for action in host_actions:
if action.get('type') == 'transfer': if action.get('type') == 'transfer':
@ -171,7 +187,7 @@ def _ext2_deploy(req, helper, env):
threads, latest_exception = [], None threads, latest_exception = [], None
max_workers = max(10, os.cpu_count() * 5) max_workers = max(10, os.cpu_count() * 5)
with futures.ThreadPoolExecutor(max_workers=max_workers) as executor: with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
for h_id in json.loads(req.host_ids): for h_id in req.host_ids:
new_env = AttrDict(env.items()) new_env = AttrDict(env.items())
t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, new_env, req.spug_version) t = executor.submit(_deploy_ext2_host, helper, h_id, host_actions, new_env, req.spug_version)
t.h_id = h_id t.h_id = h_id
@ -182,21 +198,25 @@ def _ext2_deploy(req, helper, env):
latest_exception = exception latest_exception = exception
if not isinstance(exception, SpugError): if not isinstance(exception, SpugError):
helper.send_error(t.h_id, f'Exception: {exception}', False) helper.send_error(t.h_id, f'Exception: {exception}', False)
else:
req.fail_host_ids.remove(t.h_id)
if latest_exception: if latest_exception:
raise latest_exception raise latest_exception
else: else:
host_ids = sorted(json.loads(req.host_ids), reverse=True) host_ids = sorted(req.host_ids, reverse=True)
while host_ids: while host_ids:
h_id = host_ids.pop() h_id = host_ids.pop()
new_env = AttrDict(env.items()) new_env = AttrDict(env.items())
try: try:
_deploy_ext2_host(helper, h_id, host_actions, new_env, req.spug_version) _deploy_ext2_host(helper, h_id, host_actions, new_env, req.spug_version)
req.fail_host_ids.remove(h_id)
except Exception as e: except Exception as e:
helper.send_error(h_id, f'Exception: {e}', False) helper.send_error(h_id, f'Exception: {e}', False)
for h_id in host_ids: for h_id in host_ids:
helper.send_error(h_id, '终止发布', False) helper.send_error(h_id, '终止发布', False)
raise e raise e
else: else:
req.fail_host_ids = []
helper.send_step('local', 100, f'\r\n{human_time()} ** 发布成功 **') helper.send_step('local', 100, f'\r\n{human_time()} ** 发布成功 **')

View File

@ -46,6 +46,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['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)
@ -138,6 +139,7 @@ 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)
query = {'pk': r_id} query = {'pk': r_id}
if not request.user.is_supper: if not request.user.is_supper:
perms = request.user.deploy_perms perms = request.user.deploy_perms
@ -148,14 +150,16 @@ class RequestDetailView(View):
return json_response(error='未找到指定发布申请') return json_response(error='未找到指定发布申请')
if req.status not in ('1', '-3'): if req.status not in ('1', '-3'):
return json_response(error='该申请单当前状态还不能执行发布') return json_response(error='该申请单当前状态还不能执行发布')
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
host_ids = req.fail_host_ids if form.mode == 'fail' else req.host_ids
hosts = Host.objects.filter(id__in=json.loads(host_ids))
message = f'{human_time()} 等待调度... ' message = f'{human_time()} 等待调度... '
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': message} for x in hosts} outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': message} for x in hosts}
req.status = '2' req.status = '2'
req.do_at = human_datetime() req.do_at = human_datetime()
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, form.mode == 'fail')).start()
if req.is_quick_deploy: if req.is_quick_deploy:
if req.repository_id: if req.repository_id:
outputs['local'] = {'id': 'local', 'step': 100, 'data': f'{human_time()} 已构建完成忽略执行。'} outputs['local'] = {'id': 'local', 'step': 100, 'data': f'{human_time()} 已构建完成忽略执行。'}
@ -325,6 +329,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['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)

View File

@ -23,7 +23,7 @@ def dispatch(rep: Repository, helper=None):
if not helper: if not helper:
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}'
helper = Helper(rds, rds_key) helper = Helper.make(rds, rds_key)
rep.save() rep.save()
try: try:
api_token = uuid.uuid4().hex api_token = uuid.uuid4().hex

View File

@ -36,7 +36,7 @@ function Ext1Console(props) {
function doDeploy() { function doDeploy() {
let socket; let socket;
http.post(`/api/deploy/request/${props.request.id}/`) http.post(`/api/deploy/request/${props.request.id}/`, {mode: props.request.mode})
.then(res => { .then(res => {
Object.assign(outputs, res.outputs) Object.assign(outputs, res.outputs)
setTimeout(() => setFetching(false), 100) setTimeout(() => setFetching(false), 100)
@ -57,6 +57,7 @@ function Ext1Console(props) {
} else { } else {
index += 1; index += 1;
const {key, data, step, status} = JSON.parse(e.data); const {key, data, step, status} = JSON.parse(e.data);
if (!outputs[key]) return
if (data !== undefined) { if (data !== undefined) {
outputs[key].data += data outputs[key].data += data
if (terms[key]) terms[key].write(data) if (terms[key]) terms[key].write(data)

View File

@ -40,7 +40,7 @@ function Ext2Console(props) {
function doDeploy() { function doDeploy() {
let socket; let socket;
http.post(`/api/deploy/request/${props.request.id}/`) http.post(`/api/deploy/request/${props.request.id}/`, {mode: props.request.mode})
.then(res => { .then(res => {
setSActions(res.s_actions); setSActions(res.s_actions);
setHActions(res.h_actions); setHActions(res.h_actions);
@ -63,6 +63,7 @@ function Ext2Console(props) {
} else { } else {
index += 1; index += 1;
const {key, data, step, status} = JSON.parse(e.data); const {key, data, step, status} = JSON.parse(e.data);
if (!outputs[key]) return
if (data !== undefined) { if (data !== undefined) {
outputs[key].data += data outputs[key].data += data
if (terms[key]) terms[key].write(data) if (terms[key]) terms[key].write(data)

View File

@ -12,6 +12,16 @@ import { Action, AuthButton, TableCard } from 'components';
import S from './index.module.less'; import S from './index.module.less';
import store from './store'; import store from './store';
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 columns = [{ const columns = [{
title: '申请标题', title: '申请标题',
@ -121,9 +131,7 @@ function ComTable() {
case '-3': case '-3':
return <Action> return <Action>
<Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button> <Action.Button auth="deploy.request.do" onClick={() => store.readConsole(info)}>查看</Action.Button>
<Popconfirm title="确认要执行该发布申请?" onConfirm={e => handleDeploy(e, info)}> <DoAction info={info}/>
<Action.Button auth="deploy.request.do">发布</Action.Button>
</Popconfirm>
{info.visible_rollback && ( {info.visible_rollback && (
<Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button> <Action.Button auth="deploy.request.do" onClick={() => store.rollback(info)}>回滚</Action.Button>
)} )}
@ -148,9 +156,7 @@ function ComTable() {
</Action>; </Action>;
case '1': case '1':
return <Action> return <Action>
<Popconfirm title="确认要执行该发布申请?" onConfirm={e => handleDeploy(e, info)}> <DoAction info={info}/>
<Action.Button auth="deploy.request.do">发布</Action.Button>
</Popconfirm>
<Action.Button auth="deploy.request.del" onClick={() => handleDelete(info)}>删除</Action.Button> <Action.Button auth="deploy.request.del" onClick={() => handleDelete(info)}>删除</Action.Button>
</Action>; </Action>;
case '2': case '2':
@ -163,6 +169,21 @@ function ComTable() {
} }
}]; }];
function DoAction(props) {
const {host_ids, fail_host_ids} = props.info;
return (
<Popconfirm
title={<DeployConfirm/>}
okText="全量"
cancelText="补偿"
cancelButtonProps={{disabled: [0, host_ids.length].includes(fail_host_ids.length)}}
onConfirm={e => handleDeploy(e, props.info, 'all')}
onCancel={e => handleDeploy(e, props.info, 'fail')}>
<Action.Button auth="deploy.request.do">发布</Action.Button>
</Popconfirm>
)
}
function handleDelete(info) { function handleDelete(info) {
Modal.confirm({ Modal.confirm({
title: '删除确认', title: '删除确认',
@ -177,14 +198,15 @@ function ComTable() {
}) })
} }
function handleDeploy(e, info) { function handleDeploy(e, info, mode) {
info.mode = mode
store.showConsole(info); store.showConsole(info);
} }
return ( return (
<TableCard <TableCard
tKey="dr" tKey="dr"
rowKey="id" rowKey={row => row.key || row.id}
title="申请列表" title="申请列表"
columns={columns} columns={columns}
scroll={{x: 1500}} scroll={{x: 1500}}

View File

@ -13,6 +13,7 @@
bottom: 12px; bottom: 12px;
right: 24px; right: 24px;
align-items: flex-end; align-items: flex-end;
z-index: 999;
.item { .item {
width: 180px; width: 180px;

View File

@ -58,7 +58,7 @@ class Store {
.then(res => { .then(res => {
for (let item of this.records) { for (let item of this.records) {
if (item.id === id) { if (item.id === id) {
Object.assign(item, res) Object.assign(item, res, {key: Date.now()})
break break
} }
} }