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