Commit c3af966b authored by ibuler's avatar ibuler

Merge branch 'dev' of https://github.com/jumpserver/coco into dev

parents 73757b36 97865db8
...@@ -4,8 +4,7 @@ env/ ...@@ -4,8 +4,7 @@ env/
*.pyo *.pyo
.access_key .access_key
*.log *.log
logs/* data/*
host_rsa_key host_rsa_key
sessions/*
coco.pid coco.pid
config.yml config.yml
...@@ -6,7 +6,7 @@ WORKDIR /opt/coco ...@@ -6,7 +6,7 @@ WORKDIR /opt/coco
RUN yum -y install epel-release RUN yum -y install epel-release
RUN cd requirements && yum -y install $(cat rpm_requirements.txt) RUN cd requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd requirements && pip install $(egrep "jumpserver|jms" requirements.txt | tr '\n' ' ') && pip install -r requirements.txt -i https://mirrors.ustc.edu.cn/pypi/web/simple RUN cd requirements && pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ || pip install -r requirements.txt
ENV LANG=zh_CN.UTF-8 ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8 ENV LC_ALL=zh_CN.UTF-8
...@@ -17,4 +17,4 @@ VOLUME /opt/coco/data ...@@ -17,4 +17,4 @@ VOLUME /opt/coco/data
RUN echo > config.yml RUN echo > config.yml
EXPOSE 2222 EXPOSE 2222
CMD python run_server.py ENTRYPOINT ["./entrypoint.sh"]
...@@ -10,8 +10,6 @@ import json ...@@ -10,8 +10,6 @@ import json
import signal import signal
import copy import copy
import psutil
from .conf import config from .conf import config
from .sshd import SSHServer from .sshd import SSHServer
from .httpd import HttpServer from .httpd import HttpServer
...@@ -23,7 +21,7 @@ from .session import Session ...@@ -23,7 +21,7 @@ from .session import Session
from .models import Connection from .models import Connection
__version__ = '1.4.6' __version__ = '1.4.8'
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -39,6 +37,7 @@ class Coco: ...@@ -39,6 +37,7 @@ class Coco:
self.replay_recorder_class = None self.replay_recorder_class = None
self.command_recorder_class = None self.command_recorder_class = None
self._task_handler = None self._task_handler = None
self.first_load_extra_conf = True
@property @property
def sshd(self): def sshd(self):
...@@ -58,17 +57,18 @@ class Coco: ...@@ -58,17 +57,18 @@ class Coco:
self._task_handler = TaskHandler() self._task_handler = TaskHandler()
return self._task_handler return self._task_handler
@staticmethod
@ignore_error @ignore_error
def load_extra_conf_from_server(): def load_extra_conf_from_server(self):
configs = app_service.load_config_from_server() configs = app_service.load_config_from_server()
config.update(configs) config.update(configs)
tmp = copy.deepcopy(configs) tmp = copy.deepcopy(configs)
tmp['HOST_KEY'] = tmp['HOST_KEY'][32:50] + '...' tmp['HOST_KEY'] = tmp.get('HOST_KEY', '')[32:50] + '...'
logger.debug("Loading config from server: {}".format( if self.first_load_extra_conf:
json.dumps(tmp) logger.debug("Loading config from server: {}".format(
)) json.dumps(tmp)
))
self.first_load_extra_conf = False
def keep_load_extra_conf(self): def keep_load_extra_conf(self):
def func(): def func():
...@@ -83,24 +83,25 @@ class Coco: ...@@ -83,24 +83,25 @@ class Coco:
self.keep_load_extra_conf() self.keep_load_extra_conf()
self.keep_heartbeat() self.keep_heartbeat()
self.monitor_sessions() self.monitor_sessions()
self.monitor_sessions_replay() if config.UPLOAD_FAILED_REPLAY_ON_START:
self.upload_failed_replay()
# @ignore_error # @ignore_error
def heartbeat(self): def heartbeat(self):
sessions = list(Session.sessions.keys()) sessions = list(Session.sessions.keys())
p = psutil.Process(os.getpid()) # p = psutil.Process(os.getpid())
cpu_used = p.cpu_percent(interval=1.0) # cpu_used = p.cpu_percent(interval=1.0)
memory_used = int(p.memory_info().rss / 1024 / 1024) # memory_used = int(p.memory_info().rss / 1024 / 1024)
connections = len(p.connections()) # connections = len(p.connections())
threads = p.num_threads() # threads = p.num_threads()
session_online = len(sessions) # session_online = len(sessions)
data = { data = {
"cpu_used": cpu_used, # "cpu_used": cpu_used,
"memory_used": memory_used, # "memory_used": memory_used,
"connections": connections, # "connections": connections,
"threads": threads, # "threads": threads,
"boot_time": p.create_time(), # "boot_time": p.create_time(),
"session_online": session_online, # "session_online": session_online,
"sessions": sessions, "sessions": sessions,
} }
tasks = app_service.terminal_heartbeat(data) tasks = app_service.terminal_heartbeat(data)
...@@ -131,28 +132,42 @@ class Coco: ...@@ -131,28 +132,42 @@ class Coco:
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.start() thread.start()
def monitor_sessions_replay(self): @staticmethod
interval = 10 def upload_failed_replay():
log_dir = os.path.join(config['LOG_DIR']) replay_dir = os.path.join(config.REPLAY_DIR)
def retry_upload_replay(session_id, file_gz_path, target):
recorder = get_replay_recorder()
recorder.file_gz_path = file_gz_path
recorder.session_id = session_id
recorder.target = target
recorder.upload_replay()
def check_replay_is_need_upload(full_path):
filename = os.path.basename(full_path)
suffix = filename.split('.')[-1]
if suffix != 'gz':
return False
session_id = filename.split('.')[0]
if len(session_id) != 36:
return False
return True
def func(): def func():
while not self.stop_evt.is_set(): if not os.path.isdir(replay_dir):
active_sessions = [sid for sid in Session.sessions] return
for filename in os.listdir(log_dir): for d in os.listdir(replay_dir):
date_path = os.path.join(replay_dir, d)
for filename in os.listdir(date_path):
full_path = os.path.join(date_path, filename)
session_id = filename.split('.')[0] session_id = filename.split('.')[0]
full_path = os.path.join(log_dir, filename) # 检查是否需要上传
if not check_replay_is_need_upload(full_path):
if len(session_id) != 36:
continue continue
logger.debug("Retry upload retain replay: {}".format(filename))
recorder = get_replay_recorder() target = os.path.join(d, filename)
if session_id not in active_sessions: retry_upload_replay(session_id, full_path, target)
recorder.file_path = full_path
ok = recorder.upload_replay(session_id, 1)
if not ok and os.path.getsize(full_path) == 0:
os.unlink(full_path)
time.sleep(1) time.sleep(1)
time.sleep(interval)
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.start() thread.start()
......
...@@ -296,6 +296,12 @@ class Config(dict): ...@@ -296,6 +296,12 @@ class Config(dict):
return value return value
value = os.environ.get(item, None) value = os.environ.get(item, None)
if value is not None: if value is not None:
if value.isdigit():
value = int(value)
elif value.lower() == 'false':
value = False
elif value.lower() == 'true':
value = True
return value return value
return self.defaults.get(item) return self.defaults.get(item)
...@@ -330,7 +336,9 @@ defaults = { ...@@ -330,7 +336,9 @@ defaults = {
'SECRET_KEY': 'SDK29K03%MM0ksf&#2', 'SECRET_KEY': 'SDK29K03%MM0ksf&#2',
'LOG_LEVEL': 'INFO', 'LOG_LEVEL': 'INFO',
'LOG_DIR': os.path.join(root_path, 'data', 'logs'), 'LOG_DIR': os.path.join(root_path, 'data', 'logs'),
'REPLAY_DIR': os.path.join(root_path, 'data', 'replays'),
'ASSET_LIST_SORT_BY': 'hostname', # hostname, ip 'ASSET_LIST_SORT_BY': 'hostname', # hostname, ip
'TELNET_REGEX': '',
'PASSWORD_AUTH': True, 'PASSWORD_AUTH': True,
'PUBLIC_KEY_AUTH': True, 'PUBLIC_KEY_AUTH': True,
'SSH_TIMEOUT': 10, 'SSH_TIMEOUT': 10,
...@@ -344,7 +352,9 @@ defaults = { ...@@ -344,7 +352,9 @@ defaults = {
'LANGUAGE_CODE': 'zh', 'LANGUAGE_CODE': 'zh',
'SECURITY_MAX_IDLE_TIME': 60, 'SECURITY_MAX_IDLE_TIME': 60,
'ASSET_LIST_PAGE_SIZE': 'auto', 'ASSET_LIST_PAGE_SIZE': 'auto',
'SFTP_ROOT': 'tmp', 'SFTP_ROOT': '/tmp',
'SFTP_SHOW_HIDDEN_FILE': False,
'UPLOAD_FAILED_REPLAY_ON_START': True
} }
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os
import re import re
import socket import socket
import telnetlib import telnetlib
...@@ -69,7 +68,7 @@ class SSHConnection: ...@@ -69,7 +68,7 @@ class SSHConnection:
look_for_keys=False, sock=sock, allow_agent=False, look_for_keys=False, sock=sock, allow_agent=False,
) )
transport = ssh.get_transport() transport = ssh.get_transport()
transport.set_keepalive(300) transport.set_keepalive(20)
except Exception as e: except Exception as e:
password_short = "None" password_short = "None"
key_fingerprint = "None" key_fingerprint = "None"
...@@ -133,7 +132,9 @@ class SSHConnection: ...@@ -133,7 +132,9 @@ class SSHConnection:
except: except:
continue continue
try: try:
sock = ssh.get_transport().open_channel( transport = ssh.get_transport()
transport.set_keep_alive(20)
sock = transport.open_channel(
'direct-tcpip', (asset.ip, asset.port), ('127.0.0.1', 0) 'direct-tcpip', (asset.ip, asset.port), ('127.0.0.1', 0)
) )
break break
...@@ -143,6 +144,17 @@ class SSHConnection: ...@@ -143,6 +144,17 @@ class SSHConnection:
class TelnetConnection: class TelnetConnection:
incorrect_pattern = re.compile(
r'incorrect|failed|失败|错误', re.I
)
username_pattern = re.compile(
r'login:?\s*$|username:?\s*$|用户名:?\s*$|账\s*号:?\s*$', re.I
)
password_pattern = re.compile(
r'Password:?\s*$|passwd:?\s*$|密\s*码:?\s*$', re.I
)
success_pattern = re.compile(r'Last\s*login|success|成功|#|\$', re.I)
custom_success_pattern = None
def __init__(self, asset, system_user, client): def __init__(self, asset, system_user, client):
self.client = client self.client = client
...@@ -150,18 +162,13 @@ class TelnetConnection: ...@@ -150,18 +162,13 @@ class TelnetConnection:
self.system_user = system_user self.system_user = system_user
self.sock = None self.sock = None
self.sel = selectors.DefaultSelector() self.sel = selectors.DefaultSelector()
self.incorrect_pattern = re.compile( if config.TELNET_REGEX:
r'incorrect|failed|失败|错误', re.I try:
) self.custom_success_pattern = re.compile(
self.username_pattern = re.compile( r'{}'.format(config.TELNET_REGEX), re.I
r'login:?\s*$|username:?\s*$|用户名:?\s*$|账\s*号:?\s*$', re.I )
) except (TypeError, ValueError):
self.password_pattern = re.compile( pass
r'Password:?\s*$|passwd:?\s*$|密\s*码:?\s*$', re.I
)
self.success_pattern = re.compile(
r'Last\s*login|success|成功|#|\$', re.I
)
def get_socket(self): def get_socket(self):
logger.debug('Get telnet server socket. {}'.format(self.client.user)) logger.debug('Get telnet server socket. {}'.format(self.client.user))
...@@ -269,7 +276,10 @@ class TelnetConnection: ...@@ -269,7 +276,10 @@ class TelnetConnection:
logger.debug(b'[Password prompt]: ' + b'>>' + raw_data + b'<<') logger.debug(b'[Password prompt]: ' + b'>>' + raw_data + b'<<')
self.sock.send(self.system_user.password.encode('utf-8') + b'\r\n') self.sock.send(self.system_user.password.encode('utf-8') + b'\r\n')
return None return None
elif self.success_pattern.search(data): elif self.success_pattern.search(data) or \
(self.custom_success_pattern and
self.custom_success_pattern.search(data)):
self.client.send(raw_data)
logger.debug(b'[Login Success prompt]: ' + b'>>' + raw_data + b'<<') logger.debug(b'[Login Success prompt]: ' + b'>>' + raw_data + b'<<')
return True return True
else: else:
......
...@@ -80,9 +80,11 @@ class InteractiveServer: ...@@ -80,9 +80,11 @@ class InteractiveServer:
# #
def display_banner(self): def display_banner(self):
default_title = _('Welcome to use Jumpserver open source fortress system')
header_title = config.get('HEADER_TITLE') or default_title
self.client.send(char.CLEAR_CHAR) self.client.send(char.CLEAR_CHAR)
self.display_logo() self.display_logo()
header = _("\n{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system {end}{R}{R}") header = _("\n{T}{T}{title} {user}, {header_title} {end}{R}{R}")
menu = [ menu = [
_("{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, Comment{end} to search login(if unique).{R}"), _("{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, Comment{end} to search login(if unique).{R}"),
_("{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} search, such as: /ip.{R}"), _("{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} search, such as: /ip.{R}"),
...@@ -95,7 +97,8 @@ class InteractiveServer: ...@@ -95,7 +97,8 @@ class InteractiveServer:
_("{T}0) Enter {green}q{end} exit.{R}") _("{T}0) Enter {green}q{end} exit.{R}")
] ]
self.client.send_unicode(header.format( self.client.send_unicode(header.format(
title="\033[1;32m", user=self.client.user, end="\033[0m", title="\033[1;32m", user=self.client.user,
header_title=header_title, end="\033[0m",
T='\t', R='\r\n\r' T='\t', R='\r\n\r'
)) ))
for item in menu: for item in menu:
...@@ -132,7 +135,6 @@ class InteractiveServer: ...@@ -132,7 +135,6 @@ class InteractiveServer:
self.display_banner() self.display_banner()
elif opt in ['r', 'R']: elif opt in ['r', 'R']:
self.refresh_assets_nodes() self.refresh_assets_nodes()
self.display_banner()
elif opt in ['h', 'H']: elif opt in ['h', 'H']:
self.display_banner() self.display_banner()
else: else:
...@@ -162,8 +164,9 @@ class InteractiveServer: ...@@ -162,8 +164,9 @@ class InteractiveServer:
self.display_assets_paging(assets) self.display_assets_paging(assets)
def refresh_assets_nodes(self): def refresh_assets_nodes(self):
self.get_user_assets_and_update_async() self.get_user_assets_and_update(cache_policy='2')
self.get_user_nodes_async() self.get_user_nodes(cache_policy='2')
self.client.send_unicode(_("Refresh done"))
def wait_until_assets_load(self): def wait_until_assets_load(self):
while self.assets is None and \ while self.assets is None and \
...@@ -316,9 +319,7 @@ class InteractiveServer: ...@@ -316,9 +319,7 @@ class InteractiveServer:
# #
def load_user_assets_from_cache(self): def load_user_assets_from_cache(self):
assets = self.__class__._user_assets_cached.get( assets = self.__class__._user_assets_cached.get(self.client.user.id)
self.client.user.id
)
self.assets = assets self.assets = assets
if assets: if assets:
self.total_asset_count = len(assets) self.total_asset_count = len(assets)
...@@ -327,8 +328,8 @@ class InteractiveServer: ...@@ -327,8 +328,8 @@ class InteractiveServer:
thread = threading.Thread(target=self.get_user_assets_and_update) thread = threading.Thread(target=self.get_user_assets_and_update)
thread.start() thread.start()
def get_user_assets_and_update(self): def get_user_assets_and_update(self, cache_policy='1'):
assets = app_service.get_user_assets(self.client.user) assets = app_service.get_user_assets(self.client.user, cache_policy=cache_policy)
assets = self.filter_system_users(assets) assets = self.filter_system_users(assets)
self.__class__._user_assets_cached[self.client.user.id] = assets self.__class__._user_assets_cached[self.client.user.id] = assets
self.load_user_assets_from_cache() self.load_user_assets_from_cache()
...@@ -341,8 +342,8 @@ class InteractiveServer: ...@@ -341,8 +342,8 @@ class InteractiveServer:
thread = threading.Thread(target=self.get_user_nodes) thread = threading.Thread(target=self.get_user_nodes)
thread.start() thread.start()
def get_user_nodes(self): def get_user_nodes(self, cache_policy='1'):
nodes = app_service.get_user_asset_groups(self.client.user) nodes = app_service.get_user_asset_groups(self.client.user, cache_policy=cache_policy)
nodes = sorted(nodes, key=lambda node: node.key) nodes = sorted(nodes, key=lambda node: node.key)
self.nodes = self.filter_system_users_of_assets_under_nodes(nodes) self.nodes = self.filter_system_users_of_assets_under_nodes(nodes)
self._construct_node_tree() self._construct_node_tree()
......
...@@ -45,12 +45,11 @@ def create_logger(): ...@@ -45,12 +45,11 @@ def create_logger():
}, },
'file': { 'file': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.handlers.TimedRotatingFileHandler', 'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'main', 'formatter': 'main',
'filename': log_path, 'filename': log_path,
'when': "D", 'maxBytes': 1024*1024*100,
'interval': 1, 'backupCount': 7,
"backupCount": 7
}, },
}, },
loggers={ loggers={
......
...@@ -385,6 +385,9 @@ class TelnetServer(BaseServer): ...@@ -385,6 +385,9 @@ class TelnetServer(BaseServer):
""" self.chan: socket object """ """ self.chan: socket object """
return getattr(self.chan, '_closed', False) return getattr(self.chan, '_closed', False)
def resize_pty(self):
pass
class Server(BaseServer): class Server(BaseServer):
""" """
......
...@@ -72,6 +72,12 @@ class ProxyServer: ...@@ -72,6 +72,12 @@ class ProxyServer:
self.server.close() self.server.close()
return return
session = Session.new_session(self.client, self.server) session = Session.new_session(self.client, self.server)
if not session:
msg = _("Connect with api server failed")
logger.error(msg)
self.client.send_unicode(msg)
self.server.close()
try: try:
session.bridge() session.bridge()
finally: finally:
......
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
# #
import threading import threading
import datetime
import time import time
import os import os
import gzip
import json import json
from copy import deepcopy from copy import deepcopy
import jms_storage import jms_storage
from .conf import config from .conf import config
from .utils import get_logger from .utils import get_logger, gzip_file
from .struct import MemoryQueue from .struct import MemoryQueue
from .service import app_service from .service import app_service
...@@ -22,14 +22,23 @@ BUF_SIZE = 1024 ...@@ -22,14 +22,23 @@ BUF_SIZE = 1024
class ReplayRecorder(object): class ReplayRecorder(object):
time_start = None time_start = None
target = None
storage = None storage = None
session_id = None
filename = None
file = None
file_path = None
filename_gz = None
file_gz_path = None
def __init__(self): def __init__(self):
super(ReplayRecorder, self).__init__()
self.file = None
self.file_path = None
self.get_storage() self.get_storage()
def get_storage(self):
conf = deepcopy(config["REPLAY_STORAGE"])
conf["SERVICE"] = app_service
self.storage = jms_storage.get_object_storage(conf)
def record(self, data): def record(self, data):
""" """
:param data: :param data:
...@@ -47,49 +56,63 @@ class ReplayRecorder(object): ...@@ -47,49 +56,63 @@ class ReplayRecorder(object):
def session_start(self, session_id): def session_start(self, session_id):
self.time_start = time.time() self.time_start = time.time()
filename = session_id + '.replay.gz' self.session_id = session_id
self.file_path = os.path.join(config['LOG_DIR'], filename) self.filename = session_id
self.file = gzip.open(self.file_path, 'at') self.filename_gz = session_id + '.replay.gz'
date = datetime.datetime.utcnow().strftime('%Y-%m-%d')
replay_dir = os.path.join(config.REPLAY_DIR, date)
if not os.path.isdir(replay_dir):
os.makedirs(replay_dir, exist_ok=True)
# 录像记录路径
self.file_path = os.path.join(replay_dir, self.filename)
# 录像压缩到的路径
self.file_gz_path = os.path.join(replay_dir, self.filename_gz)
# 录像上传上去的路径
self.target = date + '/' + self.filename_gz
self.file = open(self.file_path, 'at')
self.file.write('{') self.file.write('{')
def session_end(self, session_id): def session_end(self, session_id):
self.file.write('"0":""}') self.file.write('"0":""}')
self.file.close() self.file.close()
self.upload_replay(session_id) gzip_file(self.file_path, self.file_gz_path)
self.upload_replay_some_times()
def get_storage(self): def upload_replay_some_times(self, times=3):
conf = deepcopy(config["REPLAY_STORAGE"]) # 如果上传OSS、S3失败则尝试上传到服务器
conf["SERVICE"] = app_service
self.storage = jms_storage.get_object_storage(conf)
def upload_replay(self, session_id, times=3):
if times < 1: if times < 1:
if self.storage.type == 'jms': if self.storage.type == 'jms':
return False return False
else: self.storage = jms_storage.JMSReplayStorage(
self.storage = jms_storage.JMSReplayStorage( {"SERVICE": app_service}
{"SERVICE": app_service} )
) self.upload_replay_some_times(times=3)
self.upload_replay(session_id, times=3)
ok, msg = self.push_to_storage(session_id) ok, msg = self.upload_replay()
if not ok: if not ok:
msg = 'Failed push replay file {}: {}, try again {}'.format( msg = 'Failed push replay file {}: {}, try again {}'.format(
session_id, msg, times self.filename, msg, times
) )
logger.warn(msg) logger.warn(msg)
self.upload_replay(session_id, times-1) self.upload_replay_some_times(times - 1)
else: else:
msg = 'Success push replay file: {}'.format(session_id) msg = 'Success push replay file: {}'.format(self.session_id)
logger.debug(msg) logger.debug(msg)
self.finish_replay(3, session_id)
os.unlink(self.file_path)
return True return True
def push_to_storage(self, session_id): def upload_replay(self):
dt = time.strftime('%Y-%m-%d', time.localtime(self.time_start)) # 如果文件为空就直接删除
target = dt + '/' + session_id + '.replay.gz' if not os.path.isfile(self.file_gz_path):
return self.storage.upload(self.file_path, target) return False, 'Not found the file: {}'.format(self.file_gz_path)
if os.path.getsize(self.file_gz_path) == 0:
os.unlink(self.file_gz_path)
return True, ''
ok, msg = self.storage.upload(self.file_gz_path, self.target)
if ok:
self.finish_replay(3, self.session_id)
os.unlink(self.file_gz_path)
return ok, msg
def finish_replay(self, times, session_id): def finish_replay(self, times, session_id):
if times < 1: if times < 1:
...@@ -147,9 +170,10 @@ class CommandRecorder(object): ...@@ -147,9 +170,10 @@ class CommandRecorder(object):
if not data_set: if not data_set:
continue continue
logger.debug("Send {} commands to server".format(len(data_set))) logger.debug("Send {} commands to server".format(len(data_set)))
ok = self.storage.bulk_save(data_set) for i in range(5):
if not ok: ok = self.storage.bulk_save(data_set)
self.queue.mput(data_set) if ok:
break
thread = threading.Thread(target=func) thread = threading.Thread(target=func)
thread.daemon = True thread.daemon = True
......
...@@ -48,7 +48,14 @@ class Session: ...@@ -48,7 +48,14 @@ class Session:
session.set_command_recorder(command_recorder) session.set_command_recorder(command_recorder)
session.set_replay_recorder(replay_recorder) session.set_replay_recorder(replay_recorder)
cls.sessions[session.id] = session cls.sessions[session.id] = session
app_service.create_session(session.to_json()) _session = None
for i in range(5):
_session = app_service.create_session(session.to_json())
if _session:
break
time.sleep(0.2)
if _session is None:
return None
return session return session
@classmethod @classmethod
......
...@@ -45,7 +45,8 @@ def convert_error(func): ...@@ -45,7 +45,8 @@ def convert_error(func):
class SFTPServer(paramiko.SFTPServerInterface): class SFTPServer(paramiko.SFTPServerInterface):
root = config.SFTP_ROOT # Home or /tmp or other path, must exist on all server # Home or /tmp or other path, must exist on all server
root = config.SFTP_ROOT
def __init__(self, server, **kwargs): def __init__(self, server, **kwargs):
""" """
...@@ -234,6 +235,9 @@ class SFTPServer(paramiko.SFTPServerInterface): ...@@ -234,6 +235,9 @@ class SFTPServer(paramiko.SFTPServerInterface):
else: else:
client, rpath = self.get_sftp_client_rpath(request) client, rpath = self.get_sftp_client_rpath(request)
output = client.listdir_attr(rpath) output = client.listdir_attr(rpath)
show_hidden_file = config['SFTP_SHOW_HIDDEN_FILE']
if not show_hidden_file:
output = [attr for attr in output if not attr.filename.startswith('.')]
return output return output
@convert_error @convert_error
...@@ -291,6 +295,7 @@ class SFTPServer(paramiko.SFTPServerInterface): ...@@ -291,6 +295,7 @@ class SFTPServer(paramiko.SFTPServerInterface):
try: try:
client, rpath = self.get_sftp_client_rpath(path) client, rpath = self.get_sftp_client_rpath(path)
f = client.open(rpath, mode, bufsize=4096) f = client.open(rpath, mode, bufsize=4096)
f.prefetch()
obj = paramiko.SFTPHandle(flags) obj = paramiko.SFTPHandle(flags)
obj.filename = rpath obj.filename = rpath
obj.readfile = f obj.readfile = f
......
...@@ -8,6 +8,7 @@ import logging ...@@ -8,6 +8,7 @@ import logging
import re import re
import os import os
import gettext import gettext
import gzip
from io import StringIO from io import StringIO
from binascii import hexlify from binascii import hexlify
from werkzeug.local import Local, LocalProxy from werkzeug.local import Local, LocalProxy
...@@ -464,4 +465,11 @@ def ignore_error(func): ...@@ -464,4 +465,11 @@ def ignore_error(func):
return wrapper return wrapper
def gzip_file(src_path, dst_path, unlink_ori=True):
with open(src_path, 'rt') as src, gzip.open(dst_path, 'at') as dst:
dst.writelines(src)
if unlink_ori:
os.unlink(src_path)
ugettext = LocalProxy(partial(_find, 'LANGUAGE_CODE')) ugettext = LocalProxy(partial(_find, 'LANGUAGE_CODE'))
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import os import os
import subprocess
if os.environ.get('USE_EVENTLET', '1') == '1': if os.environ.get('USE_EVENTLET', '1') == '1':
import eventlet import eventlet
...@@ -17,13 +18,30 @@ import argparse ...@@ -17,13 +18,30 @@ import argparse
import time import time
import signal import signal
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, BASE_DIR)
dirs = ('logs', 'keys') dirs = ('logs', 'keys')
for d in dirs: for d in dirs:
d2 = os.path.join('data', d) d2 = os.path.join('data', d)
if not os.path.isdir(d2): if not os.path.isdir(d2):
os.makedirs(d2) os.makedirs(d2)
from coco import Coco try:
from coco import Coco
except ImportError as e:
print("Import error: {}".format(e))
print("Sys path: {}".format(sys.path))
print("Python is: ")
print(subprocess.call('which python', shell=True))
try:
import coco
print("Coco is: {}".format(coco))
print("Coco dir: {}".format(os.listdir("coco")))
except:
pass
raise
try: try:
from coco.conf import config from coco.conf import config
......
...@@ -27,14 +27,14 @@ BOOTSTRAP_TOKEN: <PleasgeChangeSameWithJumpserver> ...@@ -27,14 +27,14 @@ BOOTSTRAP_TOKEN: <PleasgeChangeSameWithJumpserver>
# 加密密钥 # 加密密钥
# SECRET_KEY: null # SECRET_KEY: null
# 设置日志级别 ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'CRITICAL'] # 设置日志级别 [DEBUG, INFO, WARN, ERROR, FATAL, CRITICAL]
# LOG_LEVEL: INFO # LOG_LEVEL: INFO
# 日志存放的目录 # 日志存放的目录
# LOG_DIR: logs # LOG_DIR: logs
# SSH白名单 # SSH白名单
# ALLOW_SSH_USER: 'all' # ALLOW_SSH_USER: all
# SSH黑名单, 如果用户同时在白名单和黑名单,黑名单优先生效 # SSH黑名单, 如果用户同时在白名单和黑名单,黑名单优先生效
# BLOCK_SSH_USER: # BLOCK_SSH_USER:
...@@ -49,5 +49,11 @@ BOOTSTRAP_TOKEN: <PleasgeChangeSameWithJumpserver> ...@@ -49,5 +49,11 @@ BOOTSTRAP_TOKEN: <PleasgeChangeSameWithJumpserver>
# SSH连接超时时间 (default 15 seconds) # SSH连接超时时间 (default 15 seconds)
# SSH_TIMEOUT: 15 # SSH_TIMEOUT: 15
# 语言 = en # 语言 [en,zh]
# LANGUAGE_CODE: zh # LANGUAGE_CODE: zh
# SFTP的根目录, 可选 /tmp, Home其他自定义目录
# SFTP_ROOT: /tmp
# SFTP是否显示隐藏文件
# SFTP_SHOW_HIDDEN_FILE: false
#!/bin/bash
function cleanup()
{
local pids=`jobs -p`
if [[ "${pids}" != "" ]]; then
kill ${pids} >/dev/null 2>/dev/null
fi
}
trap cleanup EXIT
if [[ "$1" == "bash" ]];then
bash
else
python cocod start
fi
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-12-21 16:48+0800\n" "POT-Creation-Date: 2019-03-06 14:51+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n" "PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n" "Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/en/LC\n" "Language-Team: Language locale/en/LC\n"
...@@ -16,159 +16,170 @@ msgstr "" ...@@ -16,159 +16,170 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:145 #: coco/app.py:182
msgid "Connect idle more than {} minutes, disconnect" msgid "Connect idle more than {} minutes, disconnect"
msgstr "" msgstr ""
#: coco/interactive.py:84 #: coco/interactive.py:83
msgid "Welcome to use Jumpserver open source fortress system"
msgstr ""
#: coco/interactive.py:87
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"\n" "\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system " "{T}{T}{title} {user}, {header_title} {end}{R}{R}"
"{end}{R}{R}"
msgstr "" msgstr ""
#: coco/interactive.py:86 #: coco/interactive.py:89
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, " "{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
"Comment{end} to search login(if unique).{R}" "Comment{end} to search login(if unique).{R}"
msgstr "" msgstr ""
#: coco/interactive.py:87 #: coco/interactive.py:90
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} " "{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
"search, such as: /ip.{R}" "search, such as: /ip.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:88 #: coco/interactive.py:91
#, python-brace-format #, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}" msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:89 #: coco/interactive.py:92
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}" "{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:90 #: coco/interactive.py:93
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}5) Enter {green}g{end} + {green}NodeID{end} to display the host under the " "{T}5) Enter {green}g{end} + {green}NodeID{end} to display the host under the "
"node, such as g1.{R}" "node, such as g1.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:91 #: coco/interactive.py:94
#, python-brace-format #, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}" msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:92 #: coco/interactive.py:95
#, python-brace-format #, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}" msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:93 #: coco/interactive.py:96
#, python-brace-format #, python-brace-format
msgid "{T}8) Enter {green}r{end} to refresh your assets and nodes.{R}" msgid "{T}8) Enter {green}r{end} to refresh your assets and nodes.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:94 #: coco/interactive.py:97
#, python-brace-format #, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}" msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "" msgstr ""
#: coco/interactive.py:155 #: coco/interactive.py:158
msgid "Terminal does not support login rdp, please use web terminal to access" msgid "Terminal does not support login rdp, please use web terminal to access"
msgstr "" msgstr ""
#: coco/interactive.py:217 #: coco/interactive.py:169
msgid "Refresh done"
msgstr ""
#: coco/interactive.py:211
msgid "No Assets" msgid "No Assets"
msgstr "" msgstr ""
#: coco/interactive.py:280 #: coco/interactive.py:266
msgid "Tips: Enter the asset ID and log directly into the asset." msgid "ID"
msgstr "" msgstr ""
#: coco/interactive.py:281 #: coco/interactive.py:266
msgid "Page up: P/p" msgid "Hostname"
msgstr "" msgstr ""
#: coco/interactive.py:282 #: coco/interactive.py:266
msgid "Page down: Enter|N/n" msgid "IP"
msgstr "" msgstr ""
#: coco/interactive.py:283 #: coco/interactive.py:266
msgid "BACK: b/q" msgid "LoginAs"
msgstr "" msgstr ""
#: coco/interactive.py:303 #: coco/interactive.py:280
msgid "ID" msgid "Comment"
msgstr "" msgstr ""
#: coco/interactive.py:303 #: coco/interactive.py:290
msgid "Hostname" msgid "Page: {}, Count: {}, Total Page: {}, Total Count: {}"
msgstr "" msgstr ""
#: coco/interactive.py:303 #: coco/interactive.py:296
msgid "IP" msgid "Tips: Enter the asset ID and log directly into the asset."
msgstr "" msgstr ""
#: coco/interactive.py:303 #: coco/interactive.py:298
msgid "LoginAs" msgid "Page up: P/p"
msgstr "" msgstr ""
#: coco/interactive.py:317 #: coco/interactive.py:299
msgid "Comment" msgid "Page down: Enter|N/n"
msgstr "" msgstr ""
#: coco/interactive.py:326 #: coco/interactive.py:300
msgid "Page: {}, Count: {}, Total Page: {}, Total Count: {}" msgid "BACK: b/q"
msgstr "" msgstr ""
#: coco/interactive.py:398 #: coco/interactive.py:371
msgid "No Nodes" msgid "No Nodes"
msgstr "" msgstr ""
#: coco/interactive.py:402 #: coco/interactive.py:375
msgid "Node: [ ID.Name(Asset amount) ]" msgid "Node: [ ID.Name(Asset amount) ]"
msgstr "" msgstr ""
#: coco/interactive.py:404 #: coco/interactive.py:377
msgid "Tips: Enter g+NodeID to display the host under the node, such as g1" msgid "Tips: Enter g+NodeID to display the host under the node, such as g1"
msgstr "" msgstr ""
#: coco/interactive.py:412 #: coco/interactive.py:385
msgid "There is no matched node, please re-enter" msgid "There is no matched node, please re-enter"
msgstr "" msgstr ""
#: coco/interactive.py:442 #: coco/interactive.py:415
msgid "Select a login:: " msgid "Select a login:: "
msgstr "" msgstr ""
#: coco/interactive.py:465 #: coco/interactive.py:438
msgid "No system user" msgid "No system user"
msgstr "" msgstr ""
#: coco/models.py:242 #: coco/models.py:252
msgid "" msgid ""
"Warning: Failed to load filter rule, please press Ctrl + D to exit retry." "Warning: Failed to load filter rule, please press Ctrl + D to exit retry."
msgstr "" msgstr ""
#: coco/models.py:251 #: coco/models.py:261
msgid "Command `{}` is forbidden ........" msgid "Command `{}` is forbidden ........"
msgstr "" msgstr ""
#: coco/proxy.py:89 #: coco/proxy.py:76
msgid "Connect with api server failed"
msgstr ""
#: coco/proxy.py:104
msgid "No permission" msgid "No permission"
msgstr "" msgstr ""
#: coco/proxy.py:131 #: coco/proxy.py:147
msgid "Connecting to {}@{} {:.1f}" msgid "Connecting to {}@{} {:.1f}"
msgstr "" msgstr ""
#: coco/session.py:143 #: coco/session.py:154
msgid "Terminated by administrator" msgid "Terminated by administrator"
msgstr "" msgstr ""
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-12-21 16:48+0800\n" "POT-Creation-Date: 2019-03-06 14:51+0800\n"
"PO-Revision-Date: 2018-08-10 10:42+0800\n" "PO-Revision-Date: 2018-08-10 10:42+0800\n"
"Last-Translator: BaiJiangjie <bugatti_it@163.com>\n" "Last-Translator: BaiJiangjie <bugatti_it@163.com>\n"
"Language-Team: Language locale/zh\n" "Language-Team: Language locale/zh\n"
...@@ -16,21 +16,24 @@ msgstr "" ...@@ -16,21 +16,24 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:145 #: coco/app.py:182
msgid "Connect idle more than {} minutes, disconnect" msgid "Connect idle more than {} minutes, disconnect"
msgstr "空闲时间超过 {} 分钟,断开连接" msgstr "空闲时间超过 {} 分钟,断开连接"
#: coco/interactive.py:84 #: coco/interactive.py:83
msgid "Welcome to use Jumpserver open source fortress system"
msgstr "欢迎使用Jumpserver开源跳板机系统"
#: coco/interactive.py:87
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"\n" "\n"
"{T}{T}{title} {user}, Welcome to use Jumpserver open source fortress system " "{T}{T}{title} {user}, {header_title} {end}{R}{R}"
"{end}{R}{R}"
msgstr "" msgstr ""
"\n" "\n"
"{T}{T}{title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}{R}{R}" "{T}{T}{title} {user}, {header_title} {end}{R}{R}"
#: coco/interactive.py:86 #: coco/interactive.py:89
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, " "{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
...@@ -39,7 +42,7 @@ msgstr "" ...@@ -39,7 +42,7 @@ msgstr ""
"{T}1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进" "{T}1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进"
"行搜索登录(如果唯一).{R}" "行搜索登录(如果唯一).{R}"
#: coco/interactive.py:87 #: coco/interactive.py:90
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} " "{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
...@@ -48,18 +51,18 @@ msgstr "" ...@@ -48,18 +51,18 @@ msgstr ""
"{T}2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. " "{T}2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. "
"如: /ip{R}" "如: /ip{R}"
#: coco/interactive.py:88 #: coco/interactive.py:91
#, python-brace-format #, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}" msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr "{T}3) 输入 {green}p{end} 显示您有权限的主机.{R}" msgstr "{T}3) 输入 {green}p{end} 显示您有权限的主机.{R}"
#: coco/interactive.py:89 #: coco/interactive.py:92
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}" "{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr "{T}4) 输入 {green}g{end} 显示您有权限的节点.{R}" msgstr "{T}4) 输入 {green}g{end} 显示您有权限的节点.{R}"
#: coco/interactive.py:90 #: coco/interactive.py:93
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"{T}5) Enter {green}g{end} + {green}NodeID{end} to display the host under the " "{T}5) Enter {green}g{end} + {green}NodeID{end} to display the host under the "
...@@ -67,116 +70,124 @@ msgid "" ...@@ -67,116 +70,124 @@ msgid ""
msgstr "" msgstr ""
"{T}5) 输入 {green}g{end} + {green}节点ID{end} 显示节点下主机. 如: g1{R}" "{T}5) 输入 {green}g{end} + {green}节点ID{end} 显示节点下主机. 如: g1{R}"
#: coco/interactive.py:91 #: coco/interactive.py:94
#, python-brace-format #, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}" msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}" msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}"
#: coco/interactive.py:92 #: coco/interactive.py:95
#, python-brace-format #, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}" msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}" msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:93 #: coco/interactive.py:96
#, python-brace-format #, python-brace-format
msgid "{T}8) Enter {green}r{end} to refresh your assets and nodes.{R}" msgid "{T}8) Enter {green}r{end} to refresh your assets and nodes.{R}"
msgstr "{T}0) 输入 {green}r{end} 刷新最新的机器和节点信息.{R}" msgstr "{T}0) 输入 {green}r{end} 刷新最新的机器和节点信息.{R}"
#: coco/interactive.py:94 #: coco/interactive.py:97
#, python-brace-format #, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}" msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}" msgstr "{T}0) 输入 {green}q{end} 退出.{R}"
#: coco/interactive.py:155 #: coco/interactive.py:158
msgid "Terminal does not support login rdp, please use web terminal to access" msgid "Terminal does not support login rdp, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问" msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:217 #: coco/interactive.py:169
msgid "Refresh done"
msgstr "刷新完成"
#: coco/interactive.py:211
msgid "No Assets" msgid "No Assets"
msgstr "没有资产" msgstr "没有资产"
#: coco/interactive.py:280 #: coco/interactive.py:266
msgid "Tips: Enter the asset ID and log directly into the asset."
msgstr "提示: 输入资产ID,直接登录资产."
#: coco/interactive.py:281
msgid "Page up: P/p"
msgstr "上一页: P/p"
#: coco/interactive.py:282
msgid "Page down: Enter|N/n"
msgstr "下一页: Enter|N/n"
#: coco/interactive.py:283
msgid "BACK: b/q"
msgstr "返回: B/b"
#: coco/interactive.py:303
msgid "ID" msgid "ID"
msgstr "" msgstr ""
#: coco/interactive.py:303 #: coco/interactive.py:266
msgid "Hostname" msgid "Hostname"
msgstr "主机名" msgstr "主机名"
#: coco/interactive.py:303 #: coco/interactive.py:266
msgid "IP" msgid "IP"
msgstr "" msgstr ""
#: coco/interactive.py:303 #: coco/interactive.py:266
msgid "LoginAs" msgid "LoginAs"
msgstr "登录用户" msgstr "登录用户"
#: coco/interactive.py:317 #: coco/interactive.py:280
msgid "Comment" msgid "Comment"
msgstr "备注" msgstr "备注"
#: coco/interactive.py:326 #: coco/interactive.py:290
msgid "Page: {}, Count: {}, Total Page: {}, Total Count: {}" msgid "Page: {}, Count: {}, Total Page: {}, Total Count: {}"
msgstr "页码: {}, 数量: {}, 总页数: {}, 总数量: {}" msgstr "页码: {}, 数量: {}, 总页数: {}, 总数量: {}"
#: coco/interactive.py:398 #: coco/interactive.py:296
msgid "Tips: Enter the asset ID and log directly into the asset."
msgstr "提示: 输入资产ID,直接登录资产."
#: coco/interactive.py:298
msgid "Page up: P/p"
msgstr "上一页: P/p"
#: coco/interactive.py:299
msgid "Page down: Enter|N/n"
msgstr "下一页: Enter|N/n"
#: coco/interactive.py:300
msgid "BACK: b/q"
msgstr "返回: B/b"
#: coco/interactive.py:371
msgid "No Nodes" msgid "No Nodes"
msgstr "没有节点" msgstr "没有节点"
#: coco/interactive.py:402 #: coco/interactive.py:375
msgid "Node: [ ID.Name(Asset amount) ]" msgid "Node: [ ID.Name(Asset amount) ]"
msgstr "节点: [ ID.名称(资产数量) ]" msgstr "节点: [ ID.名称(资产数量) ]"
#: coco/interactive.py:404 #: coco/interactive.py:377
msgid "Tips: Enter g+NodeID to display the host under the node, such as g1" msgid "Tips: Enter g+NodeID to display the host under the node, such as g1"
msgstr "提示: 输入 g+节点ID 显示节点下主机. 如: g1" msgstr "提示: 输入 g+节点ID 显示节点下主机. 如: g1"
#: coco/interactive.py:412 #: coco/interactive.py:385
msgid "There is no matched node, please re-enter" msgid "There is no matched node, please re-enter"
msgstr "没有匹配分组,请重新输入" msgstr "没有匹配分组,请重新输入"
#: coco/interactive.py:442 #: coco/interactive.py:415
msgid "Select a login:: " msgid "Select a login:: "
msgstr "选择一个登录:" msgstr "选择一个登录:"
#: coco/interactive.py:465 #: coco/interactive.py:438
msgid "No system user" msgid "No system user"
msgstr "没有系统用户" msgstr "没有系统用户"
#: coco/models.py:242 #: coco/models.py:252
msgid "" msgid ""
"Warning: Failed to load filter rule, please press Ctrl + D to exit retry." "Warning: Failed to load filter rule, please press Ctrl + D to exit retry."
msgstr "警告: 加载过滤规则失败,请按 Ctrl + D 退出重试." msgstr "警告: 加载过滤规则失败,请按 Ctrl + D 退出重试."
#: coco/models.py:251 #: coco/models.py:261
msgid "Command `{}` is forbidden ........" msgid "Command `{}` is forbidden ........"
msgstr "命令 `{}` 是被禁止的 ..." msgstr "命令 `{}` 是被禁止的 ..."
#: coco/proxy.py:89 #: coco/proxy.py:76
msgid "Connect with api server failed"
msgstr ""
#: coco/proxy.py:104
msgid "No permission" msgid "No permission"
msgstr "没有权限" msgstr "没有权限"
#: coco/proxy.py:131 #: coco/proxy.py:147
msgid "Connecting to {}@{} {:.1f}" msgid "Connecting to {}@{} {:.1f}"
msgstr "开始连接到 {}@{} {:.1f}" msgstr "开始连接到 {}@{} {:.1f}"
#: coco/session.py:143 #: coco/session.py:154
msgid "Terminated by administrator" msgid "Terminated by administrator"
msgstr "被管理员中断" msgstr "被管理员中断"
......
...@@ -18,7 +18,7 @@ idna==2.6 ...@@ -18,7 +18,7 @@ idna==2.6
itsdangerous==0.24 itsdangerous==0.24
Jinja2==2.10 Jinja2==2.10
jmespath==0.9.3 jmespath==0.9.3
jms-storage==0.0.20 jms-storage==0.0.22
jumpserver-python-sdk==0.0.56 jumpserver-python-sdk==0.0.56
MarkupSafe==1.0 MarkupSafe==1.0
oss2==2.4.0 oss2==2.4.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment