mirror of https://github.com/openspug/spug
fix issue
parent
8a5c61b841
commit
8365365854
|
@ -158,44 +158,44 @@ def _deploy_ext1_host(req, helper, h_id, env):
|
|||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
ssh = host.get_ssh()
|
||||
code, _ = ssh.exec_command(
|
||||
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
||||
if code == 0:
|
||||
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
||||
# 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(host.id, ssh, f'cd {extend.dst_repo} && rm -rf {req.spug_version} && {clean_command}')
|
||||
# transfer files
|
||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||
try:
|
||||
ssh.put_file(os.path.join(REPOS_DIR, 'build', tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file))
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
with host.get_ssh(default_env=env) as ssh:
|
||||
code, _ = ssh.exec_command_raw(
|
||||
f'mkdir -p {extend.dst_repo} && [ -e {extend.dst_dir} ] && [ ! -L {extend.dst_dir} ]')
|
||||
if code == 0:
|
||||
helper.send_error(host.id, f'检测到该主机的发布目录 {extend.dst_dir!r} 已存在,为了数据安全请自行备份后删除该目录,Spug 将会创建并接管该目录。')
|
||||
# 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} && rm -rf {req.spug_version} && {clean_command}')
|
||||
# transfer files
|
||||
tar_gz_file = f'{req.spug_version}.tar.gz'
|
||||
try:
|
||||
ssh.put_file(os.path.join(REPOS_DIR, 'build', tar_gz_file), os.path.join(extend.dst_repo, tar_gz_file))
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
|
||||
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
||||
helper.remote(host.id, ssh, command)
|
||||
helper.send_step(h_id, 1, '完成\r\n')
|
||||
command = f'cd {extend.dst_repo} && tar xf {tar_gz_file} && rm -f {req.deploy_id}_*.tar.gz'
|
||||
helper.remote_raw(host.id, ssh, command)
|
||||
helper.send_step(h_id, 1, '完成\r\n')
|
||||
|
||||
# pre host
|
||||
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||
if extend.hook_pre_host:
|
||||
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
|
||||
command = f'cd {repo_dir} ; {extend.hook_pre_host}'
|
||||
helper.remote(host.id, ssh, command, env)
|
||||
# pre host
|
||||
repo_dir = os.path.join(extend.dst_repo, req.spug_version)
|
||||
if extend.hook_pre_host:
|
||||
helper.send_step(h_id, 2, f'{human_time()} 发布前任务... \r\n')
|
||||
command = f'cd {repo_dir} ; {extend.hook_pre_host}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
|
||||
# do deploy
|
||||
helper.send_step(h_id, 3, f'{human_time()} 执行发布... ')
|
||||
helper.remote(host.id, ssh, f'rm -f {extend.dst_dir} && ln -sfn {repo_dir} {extend.dst_dir}')
|
||||
helper.send_step(h_id, 3, '完成\r\n')
|
||||
# do deploy
|
||||
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.send_step(h_id, 3, '完成\r\n')
|
||||
|
||||
# post host
|
||||
if extend.hook_post_host:
|
||||
helper.send_step(h_id, 4, f'{human_time()} 发布后任务... \r\n')
|
||||
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
|
||||
helper.remote(host.id, ssh, command, env)
|
||||
# post host
|
||||
if extend.hook_post_host:
|
||||
helper.send_step(h_id, 4, f'{human_time()} 发布后任务... \r\n')
|
||||
command = f'cd {extend.dst_dir} ; {extend.hook_post_host}'
|
||||
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()} ** 发布成功 **')
|
||||
|
||||
|
||||
def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
||||
|
@ -204,31 +204,31 @@ def _deploy_ext2_host(helper, h_id, actions, env, spug_version):
|
|||
if not host:
|
||||
helper.send_error(h_id, 'no such host')
|
||||
env.update({'SPUG_HOST_ID': h_id, 'SPUG_HOST_NAME': host.hostname})
|
||||
ssh = host.get_ssh()
|
||||
for index, action in enumerate(actions):
|
||||
helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n')
|
||||
if action.get('type') == 'transfer':
|
||||
if action.get('src_mode') == '1':
|
||||
try:
|
||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), action['dst'])
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
helper.send_info(host.id, 'transfer completed\r\n')
|
||||
continue
|
||||
else:
|
||||
sp_dir, sd_dst = os.path.split(action['src'])
|
||||
tar_gz_file = f'{spug_version}.tar.gz'
|
||||
try:
|
||||
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}')
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
with host.get_ssh(default_env=env) as ssh:
|
||||
for index, action in enumerate(actions):
|
||||
helper.send_step(h_id, 1 + index, f'{human_time()} {action["title"]}...\r\n')
|
||||
if action.get('type') == 'transfer':
|
||||
if action.get('src_mode') == '1':
|
||||
try:
|
||||
ssh.put_file(os.path.join(REPOS_DIR, env.SPUG_DEPLOY_ID, spug_version), action['dst'])
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
helper.send_info(host.id, 'transfer completed\r\n')
|
||||
continue
|
||||
else:
|
||||
sp_dir, sd_dst = os.path.split(action['src'])
|
||||
tar_gz_file = f'{spug_version}.tar.gz'
|
||||
try:
|
||||
ssh.put_file(os.path.join(sp_dir, tar_gz_file), f'/tmp/{tar_gz_file}')
|
||||
except Exception as e:
|
||||
helper.send_error(host.id, f'exception: {e}')
|
||||
|
||||
command = f'mkdir -p /tmp/{spug_version} && tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ '
|
||||
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
|
||||
command += f'&& rm -rf /tmp/{spug_version}* && echo "transfer completed"'
|
||||
else:
|
||||
command = f'cd /tmp ; {action["data"]}'
|
||||
helper.remote(host.id, ssh, command, env)
|
||||
command = f'mkdir -p /tmp/{spug_version} && tar xf /tmp/{tar_gz_file} -C /tmp/{spug_version}/ '
|
||||
command += f'&& rm -rf {action["dst"]} && mv /tmp/{spug_version}/{sd_dst} {action["dst"]} '
|
||||
command += f'&& rm -rf /tmp/{spug_version}* && echo "transfer completed"'
|
||||
else:
|
||||
command = f'cd /tmp ; {action["data"]}'
|
||||
helper.remote(host.id, ssh, command)
|
||||
|
||||
helper.send_step(h_id, 100, f'\r\n{human_time()} ** 发布成功 **')
|
||||
|
||||
|
@ -400,7 +400,6 @@ class Helper:
|
|||
if env:
|
||||
env = dict(env.items())
|
||||
env.update(os.environ)
|
||||
command = 'set -e\n' + command
|
||||
task = subprocess.Popen(command, env=env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
while True:
|
||||
message = task.stdout.readline()
|
||||
|
@ -416,3 +415,8 @@ class Helper:
|
|||
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}')
|
||||
|
|
|
@ -124,10 +124,10 @@ class RequestDetailView(View):
|
|||
if not req:
|
||||
return json_response(error='未找到指定发布申请')
|
||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': [f'{human_time()} 读取数据... ']} for x in hosts}
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'data': f'{human_time()} 读取数据... '} for x in hosts}
|
||||
response = {'outputs': outputs, 'status': req.status}
|
||||
if req.deploy.extend == '2':
|
||||
outputs['local'] = {'id': 'local', 'data': [f'{human_time()} 读取数据... ']}
|
||||
outputs['local'] = {'id': 'local', 'data': f'{human_time()} 读取数据... '}
|
||||
response['s_actions'] = json.loads(req.deploy.extend_obj.server_actions)
|
||||
response['h_actions'] = json.loads(req.deploy.extend_obj.host_actions)
|
||||
if not response['h_actions']:
|
||||
|
@ -139,7 +139,7 @@ class RequestDetailView(View):
|
|||
for item in data:
|
||||
item = json.loads(item.decode())
|
||||
if 'data' in item:
|
||||
outputs[item['key']]['data'].append(item['data'])
|
||||
outputs[item['key']]['data'] += item['data']
|
||||
if 'step' in item:
|
||||
outputs[item['key']]['step'] = item['step']
|
||||
if 'status' in item:
|
||||
|
@ -160,7 +160,7 @@ class RequestDetailView(View):
|
|||
return json_response(error='该申请单当前状态还不能执行发布')
|
||||
hosts = Host.objects.filter(id__in=json.loads(req.host_ids))
|
||||
message = f'{human_time()} 等待调度... '
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': [message]} for x in hosts}
|
||||
outputs = {x.id: {'id': x.id, 'title': x.name, 'step': 0, 'data': message} for x in hosts}
|
||||
req.status = '2'
|
||||
req.do_at = human_datetime()
|
||||
req.do_by = request.user
|
||||
|
@ -168,7 +168,7 @@ class RequestDetailView(View):
|
|||
Thread(target=dispatch, args=(req,)).start()
|
||||
if req.deploy.extend == '2':
|
||||
message = f'{human_time()} 建立连接... '
|
||||
outputs['local'] = {'id': 'local', 'step': 0, 'data': [message]}
|
||||
outputs['local'] = {'id': 'local', 'step': 0, 'data': message}
|
||||
s_actions = json.loads(req.deploy.extend_obj.server_actions)
|
||||
h_actions = json.loads(req.deploy.extend_obj.host_actions)
|
||||
if not h_actions:
|
||||
|
|
|
@ -39,7 +39,7 @@ class Job:
|
|||
def run(self):
|
||||
if not self.token:
|
||||
with self.ssh:
|
||||
return self.ssh.exec_command_raw(self.command)
|
||||
return self.ssh.exec_command(self.command)
|
||||
self.send('\x1b[36m### Executing ...\x1b[0m\r')
|
||||
code = -1
|
||||
try:
|
||||
|
|
|
@ -24,9 +24,9 @@ class Host(models.Model, ModelMixin):
|
|||
def private_key(self):
|
||||
return self.pkey or AppSetting.get('private_key')
|
||||
|
||||
def get_ssh(self, pkey=None):
|
||||
def get_ssh(self, pkey=None, default_env=None):
|
||||
pkey = pkey or self.private_key
|
||||
return SSH(self.hostname, self.port, self.username, pkey)
|
||||
return SSH(self.hostname, self.port, self.username, pkey, default_env=default_env)
|
||||
|
||||
def to_view(self):
|
||||
tmp = self.to_dict()
|
||||
|
|
|
@ -189,7 +189,8 @@ def fetch_host_extend(ssh):
|
|||
"fdisk -l 2> /dev/null | grep '^Disk /' | awk '{print $5}'",
|
||||
"fdisk -l 2> /dev/null | grep '^磁盘 /' | awk '{print $4}' | awk -F',' '{print $2}'"
|
||||
]
|
||||
code, out = ssh.exec_command(';'.join(commands))
|
||||
with ssh:
|
||||
code, out = ssh.exec_command_raw(';'.join(commands))
|
||||
if code != 0:
|
||||
raise Exception(out)
|
||||
response = {'disk': [], 'public_ip_address': [], 'private_ip_address': []}
|
||||
|
@ -252,11 +253,11 @@ def _sync_host_extend(host, private_key=None, public_key=None, password=None, ss
|
|||
def _get_ssh(kwargs, pkey=None, private_key=None, public_key=None, password=None):
|
||||
try:
|
||||
ssh = SSH(pkey=pkey or private_key, **kwargs)
|
||||
ssh.ping()
|
||||
ssh.get_client()
|
||||
return ssh
|
||||
except AuthenticationException as e:
|
||||
if password:
|
||||
ssh = SSH(password=str(password), **kwargs)
|
||||
ssh.add_public_key(public_key)
|
||||
with SSH(password=str(password), **kwargs) as ssh:
|
||||
ssh.add_public_key(public_key)
|
||||
return _get_ssh(kwargs, private_key)
|
||||
raise e
|
||||
|
|
|
@ -56,8 +56,8 @@ def ping_check(addr):
|
|||
|
||||
def host_executor(host, command):
|
||||
try:
|
||||
cli = host.get_ssh()
|
||||
exit_code, out = cli.exec_command(command)
|
||||
with host.get_ssh() as ssh:
|
||||
exit_code, out = ssh.exec_command(command)
|
||||
if exit_code == 0:
|
||||
return True, out or '检测状态正常'
|
||||
else:
|
||||
|
|
|
@ -28,8 +28,8 @@ def local_executor(command):
|
|||
def host_executor(host, command):
|
||||
code, out, now = 1, None, time.time()
|
||||
try:
|
||||
cli = host.get_ssh()
|
||||
code, out = cli.exec_command(command)
|
||||
with host.get_ssh() as ssh:
|
||||
code, out = ssh.exec_command(command)
|
||||
except AuthenticationException:
|
||||
out = 'ssh authentication fail'
|
||||
except socket.error as e:
|
||||
|
|
|
@ -10,13 +10,15 @@ import re
|
|||
|
||||
|
||||
class SSH:
|
||||
def __init__(self, hostname, port=22, username='root', pkey=None, password=None, connect_timeout=10):
|
||||
def __init__(self, hostname, port=22, username='root', pkey=None, password=None, default_env=None,
|
||||
connect_timeout=10):
|
||||
self.stdout = None
|
||||
self.client = None
|
||||
self.channel = None
|
||||
self.sftp = None
|
||||
self.eof = 'Spug EOF 2108111926'
|
||||
self.regex = re.compile(r'Spug EOF 2108111926 \d+[\r\n]?$')
|
||||
self.default_env = self._make_env_command(default_env)
|
||||
self.regex = re.compile(r'Spug EOF 2108111926 -?\d+[\r\n]?$')
|
||||
self.arguments = {
|
||||
'hostname': hostname,
|
||||
'port': port,
|
||||
|
@ -36,22 +38,20 @@ class SSH:
|
|||
def get_client(self):
|
||||
if self.client is not None:
|
||||
return self.client
|
||||
print('\n~~ ssh start ~~')
|
||||
self.client = SSHClient()
|
||||
self.client.set_missing_host_key_policy(AutoAddPolicy)
|
||||
self.client.connect(**self.arguments)
|
||||
return self.client
|
||||
|
||||
def ping(self):
|
||||
self.get_client()
|
||||
return True
|
||||
|
||||
def add_public_key(self, public_key):
|
||||
command = f'mkdir -p -m 700 ~/.ssh && \
|
||||
echo {public_key!r} >> ~/.ssh/authorized_keys && \
|
||||
chmod 600 ~/.ssh/authorized_keys'
|
||||
_, out, _ = self.client.exec_command(command)
|
||||
if out.channel.recv_exit_status() != 0:
|
||||
exit_code, out = self.exec_command_raw(command)
|
||||
if exit_code != 0:
|
||||
raise Exception(f'add public key error: {out}')
|
||||
|
||||
def exec_command_raw(self, command):
|
||||
|
@ -69,7 +69,7 @@ class SSH:
|
|||
channel.send(command)
|
||||
out, exit_code = '', -1
|
||||
for line in self.stdout:
|
||||
if line.startswith(self.eof):
|
||||
if self.regex.search(line):
|
||||
exit_code = int(line.rsplit()[-1])
|
||||
break
|
||||
out += line
|
||||
|
@ -91,10 +91,6 @@ class SSH:
|
|||
yield exit_code, line
|
||||
yield exit_code, line
|
||||
|
||||
def get_file(self, file):
|
||||
sftp = self._get_sftp()
|
||||
return sftp.open(file)
|
||||
|
||||
def put_file(self, local_path, remote_path):
|
||||
sftp = self._get_sftp()
|
||||
sftp.put(local_path, remote_path)
|
||||
|
@ -115,13 +111,17 @@ class SSH:
|
|||
if self.channel:
|
||||
return self.channel
|
||||
|
||||
counter, data = 0, ''
|
||||
counter = 0
|
||||
self.channel = self.client.invoke_shell()
|
||||
self.channel.send(b'export PS1= && stty -echo && echo Spug execute start\n')
|
||||
command = 'export PS1= && stty -echo'
|
||||
if self.default_env:
|
||||
command += f' && {self.default_env}'
|
||||
command += f' && echo {self.eof} $?\n'
|
||||
self.channel.send(command.encode())
|
||||
while True:
|
||||
if self.channel.recv_ready():
|
||||
data += self.channel.recv(8196).decode()
|
||||
if 'Spug execute start\r\n' in data:
|
||||
line = self.channel.recv(8196).decode()
|
||||
if self.regex.search(line):
|
||||
self.stdout = self.channel.makefile('r')
|
||||
break
|
||||
elif counter >= 100:
|
||||
|
@ -168,6 +168,5 @@ class SSH:
|
|||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
print('close √')
|
||||
self.client.close()
|
||||
self.client = None
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer, useLocalStore } from 'mobx-react';
|
||||
import { Card, Progress, Modal, Collapse, Steps } from 'antd';
|
||||
import { Card, Progress, Modal, Collapse, Steps, Skeleton } from 'antd';
|
||||
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import OutView from './OutView';
|
||||
import { http, X_TOKEN } from 'libs';
|
||||
|
@ -14,6 +14,8 @@ import store from './store';
|
|||
|
||||
function Ext1Console(props) {
|
||||
const outputs = useLocalStore(() => ({}));
|
||||
const terms = useLocalStore(() => ({}));
|
||||
const [fetching, setFetching] = useState(true);
|
||||
|
||||
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
|
||||
|
||||
|
@ -22,6 +24,7 @@ function Ext1Console(props) {
|
|||
http.get(`/api/deploy/request/${props.request.id}/`)
|
||||
.then(res => {
|
||||
Object.assign(outputs, res.outputs)
|
||||
setTimeout(() => setFetching(false), 100)
|
||||
if (res.status === '2') {
|
||||
socket = _makeSocket()
|
||||
}
|
||||
|
@ -34,6 +37,7 @@ function Ext1Console(props) {
|
|||
http.post(`/api/deploy/request/${props.request.id}/`)
|
||||
.then(res => {
|
||||
Object.assign(outputs, res.outputs)
|
||||
setTimeout(() => setFetching(false), 100)
|
||||
socket = _makeSocket()
|
||||
store.fetchRecords()
|
||||
})
|
||||
|
@ -52,7 +56,10 @@ function Ext1Console(props) {
|
|||
} else {
|
||||
index += 1;
|
||||
const {key, data, step, status} = JSON.parse(e.data);
|
||||
if (data !== undefined) outputs[key].data.push(data);
|
||||
if (data !== undefined) {
|
||||
outputs[key].data += data
|
||||
if (terms[key]) terms[key].write(data)
|
||||
}
|
||||
if (step !== undefined) outputs[key].step = step;
|
||||
if (status !== undefined) outputs[key].status = status;
|
||||
}
|
||||
|
@ -73,6 +80,13 @@ function Ext1Console(props) {
|
|||
store.tabModes[props.request.id] = !value
|
||||
}
|
||||
|
||||
function handleSetTerm(term, key) {
|
||||
if (outputs[key] && outputs[key].data) {
|
||||
term.write(outputs[key].data)
|
||||
}
|
||||
terms[key] = term
|
||||
}
|
||||
|
||||
return store.tabModes[props.request.id] ? (
|
||||
<Card
|
||||
className={styles.item}
|
||||
|
@ -103,29 +117,30 @@ function Ext1Console(props) {
|
|||
<ShrinkOutlined/>
|
||||
</div>
|
||||
]}>
|
||||
<Collapse
|
||||
defaultActiveKey={'0'}
|
||||
className={styles.collapse}
|
||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||
{Object.values(outputs).map((item, index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}>{item.title}</b>
|
||||
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||
<StepItem title="等待调度" item={item} step={0}/>
|
||||
<StepItem title="数据准备" item={item} step={1}/>
|
||||
<StepItem title="发布前任务" item={item} step={2}/>
|
||||
<StepItem title="执行发布" item={item} step={3}/>
|
||||
<StepItem title="发布后任务" item={item} step={4}/>
|
||||
</Steps>
|
||||
</div>}>
|
||||
<OutView records={item.data}/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
|
||||
</Collapse>
|
||||
<Skeleton loading={fetching} active>
|
||||
<Collapse
|
||||
defaultActiveKey="0"
|
||||
className={styles.collapse}
|
||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||
{Object.entries(outputs).map(([key, item], index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}>{item.title}</b>
|
||||
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||
<StepItem title="等待调度" item={item} step={0}/>
|
||||
<StepItem title="数据准备" item={item} step={1}/>
|
||||
<StepItem title="发布前任务" item={item} step={2}/>
|
||||
<StepItem title="执行发布" item={item} step={3}/>
|
||||
<StepItem title="发布后任务" item={item} step={4}/>
|
||||
</Steps>
|
||||
</div>}>
|
||||
<OutView setTerm={term => handleSetTerm(term, key)}/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
</Skeleton>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer, useLocalStore } from 'mobx-react';
|
||||
import { Card, Progress, Modal, Collapse, Steps } from 'antd';
|
||||
import { Card, Progress, Modal, Collapse, Steps, Skeleton } from 'antd';
|
||||
import { ShrinkOutlined, CaretRightOutlined, LoadingOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import OutView from './OutView';
|
||||
import { http, X_TOKEN } from 'libs';
|
||||
|
@ -13,9 +13,11 @@ import styles from './index.module.less';
|
|||
import store from './store';
|
||||
|
||||
function Ext2Console(props) {
|
||||
const terms = useLocalStore(() => ({}));
|
||||
const outputs = useLocalStore(() => ({local: {id: 'local'}}));
|
||||
const [sActions, setSActions] = useState([]);
|
||||
const [hActions, setHActions] = useState([]);
|
||||
const [fetching, setFetching] = useState(true);
|
||||
|
||||
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
|
||||
|
||||
|
@ -25,7 +27,8 @@ function Ext2Console(props) {
|
|||
.then(res => {
|
||||
setSActions(res.s_actions);
|
||||
setHActions(res.h_actions);
|
||||
Object.assign(outputs, res.outputs)
|
||||
Object.assign(outputs, res.outputs);
|
||||
setTimeout(() => setFetching(false), 100)
|
||||
if (res.status === '2') {
|
||||
socket = _makeSocket()
|
||||
}
|
||||
|
@ -40,6 +43,7 @@ function Ext2Console(props) {
|
|||
setSActions(res.s_actions);
|
||||
setHActions(res.h_actions);
|
||||
Object.assign(outputs, res.outputs)
|
||||
setTimeout(() => setFetching(false), 100)
|
||||
socket = _makeSocket()
|
||||
})
|
||||
return () => socket && socket.close()
|
||||
|
@ -57,7 +61,10 @@ function Ext2Console(props) {
|
|||
} else {
|
||||
index += 1;
|
||||
const {key, data, step, status} = JSON.parse(e.data);
|
||||
if (data !== undefined) outputs[key].data.push(data);
|
||||
if (data !== undefined) {
|
||||
outputs[key].data += data
|
||||
if (terms[key]) terms[key].write(data)
|
||||
}
|
||||
if (step !== undefined) outputs[key].step = step;
|
||||
if (status !== undefined) outputs[key].status = status;
|
||||
}
|
||||
|
@ -80,6 +87,13 @@ function Ext2Console(props) {
|
|||
store.tabModes[props.request.id] = !value
|
||||
}
|
||||
|
||||
function handleSetTerm(term, key) {
|
||||
if (outputs[key] && outputs[key].data) {
|
||||
term.write(outputs[key].data)
|
||||
}
|
||||
terms[key] = term
|
||||
}
|
||||
|
||||
const hostOutputs = Object.values(outputs).filter(x => x.id !== 'local');
|
||||
return store.tabModes[props.request.id] ? (
|
||||
<Card
|
||||
|
@ -113,45 +127,48 @@ function Ext2Console(props) {
|
|||
<ShrinkOutlined/>
|
||||
</div>
|
||||
]}>
|
||||
<Collapse defaultActiveKey="0" className={styles.collapse}>
|
||||
<Collapse.Panel header={(
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}/>
|
||||
<Steps size="small" className={styles.step} current={outputs.local.step} status={outputs.local.status}>
|
||||
<StepItem title="建立连接" item={outputs.local} step={0}/>
|
||||
{sActions.map((item, index) => (
|
||||
<StepItem key={index} title={item.title} item={outputs.local} step={index + 1}/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
)}>
|
||||
<OutView records={outputs.local.data}/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
{hostOutputs.length > 0 && (
|
||||
<Collapse
|
||||
defaultActiveKey="0"
|
||||
className={styles.collapse}
|
||||
style={{marginTop: 24}}
|
||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||
{hostOutputs.map((item, index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}>{item.title}</b>
|
||||
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||
<StepItem title="等待调度" item={item} step={0}/>
|
||||
{hActions.map((action, index) => (
|
||||
<StepItem key={index} title={action.title} item={item} step={index + 1}/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>}>
|
||||
<OutView records={item.data}/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
<Skeleton loading={fetching} active>
|
||||
<Collapse defaultActiveKey={['0']} className={styles.collapse}>
|
||||
<Collapse.Panel header={(
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}/>
|
||||
<Steps size="small" className={styles.step} current={outputs.local.step} status={outputs.local.status}>
|
||||
<StepItem title="建立连接" item={outputs.local} step={0}/>
|
||||
{sActions.map((item, index) => (
|
||||
<StepItem key={index} title={item.title} item={outputs.local} step={index + 1}/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>
|
||||
)}>
|
||||
<OutView setTerm={term => handleSetTerm(term, 'local')}/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
)}
|
||||
{hostOutputs.length > 0 && (
|
||||
<Collapse
|
||||
accordion
|
||||
defaultActiveKey="0"
|
||||
className={styles.collapse}
|
||||
style={{marginTop: 24}}
|
||||
expandIcon={({isActive}) => <CaretRightOutlined style={{fontSize: 16}} rotate={isActive ? 90 : 0}/>}>
|
||||
{hostOutputs.map((item, index) => (
|
||||
<Collapse.Panel
|
||||
key={index}
|
||||
header={
|
||||
<div className={styles.header}>
|
||||
<b className={styles.title}>{item.title}</b>
|
||||
<Steps size="small" className={styles.step} current={item.step} status={item.status}>
|
||||
<StepItem title="等待调度" item={item} step={0}/>
|
||||
{hActions.map((action, index) => (
|
||||
<StepItem key={index} title={action.title} item={item} step={index + 1}/>
|
||||
))}
|
||||
</Steps>
|
||||
</div>}>
|
||||
<OutView setTerm={term => handleSetTerm(term, item.id)}/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
</Collapse>
|
||||
)}
|
||||
</Skeleton>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,18 +3,28 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import styles from './index.module.less';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
function OutView(props) {
|
||||
const el = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (el) el.current.scrollTop = el.current.scrollHeight
|
||||
})
|
||||
const fitPlugin = new FitAddon()
|
||||
const term = new Terminal({disableStdin: true})
|
||||
term.loadAddon(fitPlugin)
|
||||
term.setOption('theme', {background: '#fff', foreground: '#000', selection: '#999'})
|
||||
term.open(el.current)
|
||||
props.setTerm(term)
|
||||
fitPlugin.fit()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<pre ref={el} className={styles.out}>{props.records}</pre>
|
||||
<div style={{padding: '8px 0 0 15px'}}>
|
||||
<div ref={el} style={{height: 300}}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -92,12 +92,6 @@
|
|||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.out {
|
||||
min-height: 40px;
|
||||
max-height: 400px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse :global(.ant-collapse-content-box) {
|
||||
|
|
|
@ -18,17 +18,16 @@ function OutView(props) {
|
|||
term.open(el.current)
|
||||
const data = props.getOutput()
|
||||
if (data) term.write(data)
|
||||
term.fit = () => {
|
||||
const dimensions = fitPlugin.proposeDimensions()
|
||||
if (dimensions.cols && dimensions.rows) fitPlugin.fit()
|
||||
}
|
||||
term.fit = () => fitPlugin.fit()
|
||||
props.setTerm(term)
|
||||
fitPlugin.fit()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={el} style={{padding: '10px 15px'}}/>
|
||||
<div style={{padding: '8px 0 0 15px'}}>
|
||||
<div ref={el}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue