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