Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
C
coco
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ops
coco
Commits
7a7b265e
Commit
7a7b265e
authored
Mar 18, 2019
by
ibuler
Browse files
Options
Browse Files
Download
Plain Diff
[Update] merge
parents
8db5dd32
34df7fde
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
445 additions
and
308 deletions
+445
-308
.gitignore
.gitignore
+1
-2
Dockerfile
Dockerfile
+4
-5
app.py
coco/app.py
+47
-39
conf.py
coco/conf.py
+17
-8
connection.py
coco/connection.py
+29
-19
interactive.py
coco/interactive.py
+13
-12
logger.py
coco/logger.py
+8
-5
models.py
coco/models.py
+3
-0
proxy.py
coco/proxy.py
+7
-1
recorder.py
coco/recorder.py
+57
-33
session.py
coco/session.py
+8
-1
sftp.py
coco/sftp.py
+7
-2
utils.py
coco/utils.py
+72
-0
cocod
cocod
+23
-4
conf_docker.py
conf_docker.py
+0
-73
config_example.yml
config_example.yml
+9
-3
entrypoint.sh
entrypoint.sh
+17
-0
coco.mo
locale/en/LC_MESSAGES/coco.mo
+0
-0
coco.po
locale/en/LC_MESSAGES/coco.po
+58
-47
coco.mo
locale/zh_CN/LC_MESSAGES/coco.mo
+0
-0
coco.po
locale/zh_CN/LC_MESSAGES/coco.po
+61
-50
requirements.txt
requirements/requirements.txt
+4
-4
No files found.
.gitignore
View file @
7a7b265e
...
...
@@ -4,8 +4,7 @@ env/
*.pyo
.access_key
*.log
logs
/*
data
/*
host_rsa_key
sessions/*
coco.pid
config.yml
Dockerfile
View file @
7a7b265e
...
...
@@ -6,16 +6,15 @@ 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
COPY
. /opt/coco
VOLUME
/opt/coco/logs
VOLUME
/opt/coco/keys
VOLUME
/opt/coco/data
RUN
cp
config_example.yml
config.yml
RUN
echo
>
config.yml
EXPOSE
2222
CMD
python run_server.py
ENTRYPOINT
["./entrypoint.sh"]
coco/app.py
View file @
7a7b265e
...
...
@@ -16,14 +16,17 @@ from .conf import config
from
.sshd
import
SSHServer
from
.httpd
import
HttpServer
from
.tasks
import
TaskHandler
from
.utils
import
get_logger
,
ugettext
as
_
,
ignore_error
from
.utils
import
(
get_logger
,
ugettext
as
_
,
ignore_error
,
get_monitor_data
,
get_coco_monitor_data
)
from
.service
import
app_service
from
.recorder
import
get_replay_recorder
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 +42,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 +62,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,25 +88,14 @@ 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
)
data
=
{
"cpu_used"
:
cpu_used
,
"memory_used"
:
memory_used
,
"connections"
:
connections
,
"threads"
:
threads
,
"boot_time"
:
p
.
create_time
(),
"session_online"
:
session_online
,
"sessions"
:
sessions
,
'sessions'
:
sessions
,
}
tasks
=
app_service
.
terminal_heartbeat
(
data
)
...
...
@@ -131,28 +125,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
()
...
...
coco/conf.py
View file @
7a7b265e
...
...
@@ -212,14 +212,16 @@ class Config(dict):
if
self
.
root_path
:
filename
=
os
.
path
.
join
(
self
.
root_path
,
filename
)
try
:
with
open
(
filename
)
as
json_file
:
obj
=
yaml
.
load
(
json_file
)
with
open
(
filename
)
as
f
:
obj
=
yaml
.
load
(
f
)
except
IOError
as
e
:
if
silent
and
e
.
errno
in
(
errno
.
ENOENT
,
errno
.
EISDIR
):
return
False
e
.
strerror
=
'Unable to load configuration file (
%
s)'
%
e
.
strerror
raise
return
self
.
from_mapping
(
obj
)
if
obj
:
return
self
.
from_mapping
(
obj
)
return
True
def
from_mapping
(
self
,
*
mapping
,
**
kwargs
):
"""Updates the config like :meth:`update` ignoring items with non-upper
...
...
@@ -294,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
)
...
...
@@ -328,10 +336,12 @@ defaults = {
'SECRET_KEY'
:
'SDK29K03
%
MM0ksf'
,
'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'
:
1
0
,
'SSH_TIMEOUT'
:
1
5
,
'ALLOW_SSH_USER'
:
[],
'BLOCK_SSH_USER'
:
[],
'HEARTBEAT_INTERVAL'
:
20
,
...
...
@@ -342,10 +352,9 @@ defaults = {
'LANGUAGE_CODE'
:
'zh'
,
'SECURITY_MAX_IDLE_TIME'
:
60
,
'ASSET_LIST_PAGE_SIZE'
:
'auto'
,
'REDIS_HOST'
:
'127.0.0.1'
,
'REDIS_PORT'
:
6379
,
'REDIS_PASSWORD'
:
''
,
'REDIS_DB'
:
8
,
'SFTP_ROOT'
:
'/tmp'
,
'SFTP_SHOW_HIDDEN_FILE'
:
False
,
'UPLOAD_FAILED_REPLAY_ON_START'
:
True
}
...
...
coco/connection.py
View file @
7a7b265e
# -*- coding: utf-8 -*-
#
import
os
import
re
import
socket
import
telnetlib
...
...
@@ -26,13 +25,13 @@ AUTO_LOGIN = 'auto'
class
SSHConnection
:
@staticmethod
def
get_system_user_auth
(
system_user
):
def
get_system_user_auth
(
system_user
,
asset
):
"""
获取系统用户的认证信息,密码或秘钥
:return: system user have full info
"""
password
,
private_key
=
\
app_service
.
get_system_user_auth_info
(
system_user
)
app_service
.
get_system_user_auth_info
(
system_user
,
asset
)
system_user
.
password
=
password
system_user
.
private_key
=
private_key
...
...
@@ -43,7 +42,7 @@ class SSHConnection:
error
=
''
if
not
system_user
.
password
and
not
system_user
.
private_key
:
self
.
get_system_user_auth
(
system_user
)
self
.
get_system_user_auth
(
system_user
,
asset
)
if
asset
.
domain
:
sock
=
self
.
get_proxy_sock_v2
(
asset
)
...
...
@@ -69,7 +68,7 @@ class SSHConnection:
look_for_keys
=
False
,
sock
=
sock
,
allow_agent
=
False
,
)
transport
=
ssh
.
get_transport
()
transport
.
set_keepalive
(
30
0
)
transport
.
set_keepalive
(
2
0
)
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
:
...
...
coco/interactive.py
View file @
7a7b265e
...
...
@@ -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
()
...
...
coco/logger.py
View file @
7a7b265e
...
...
@@ -3,6 +3,7 @@
#
import
os
import
socket
import
logging
from
logging.config
import
dictConfig
from
.conf
import
config
as
app_config
...
...
@@ -11,7 +12,10 @@ from .conf import config as app_config
def
create_logger
():
level
=
app_config
[
'LOG_LEVEL'
]
log_dir
=
app_config
[
'LOG_DIR'
]
log_path
=
os
.
path
.
join
(
log_dir
,
'coco.log'
)
filename
=
'coco-{}.log'
.
format
(
socket
.
gethostname
())
if
not
os
.
path
.
isdir
(
log_dir
):
os
.
makedirs
(
log_dir
)
log_path
=
os
.
path
.
join
(
log_dir
,
filename
)
main_setting
=
{
'handlers'
:
[
'console'
,
'file'
],
'level'
:
level
,
...
...
@@ -41,12 +45,11 @@ def create_logger():
},
'file'
:
{
'level'
:
'DEBUG'
,
'class'
:
'logging.handlers.
Timed
RotatingFileHandler'
,
'class'
:
'logging.handlers.RotatingFileHandler'
,
'formatter'
:
'main'
,
'filename'
:
log_path
,
'when'
:
"D"
,
'interval'
:
1
,
"backupCount"
:
7
'maxBytes'
:
1024
*
1024
*
100
,
'backupCount'
:
7
,
},
},
loggers
=
{
...
...
coco/models.py
View file @
7a7b265e
...
...
@@ -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
):
"""
...
...
coco/proxy.py
View file @
7a7b265e
...
...
@@ -34,7 +34,7 @@ class ProxyServer:
:return: system user have full info
"""
password
,
private_key
=
\
app_service
.
get_system_user_auth_info
(
self
.
system_user
)
app_service
.
get_system_user_auth_info
(
self
.
system_user
,
self
.
asset
)
if
self
.
system_user
.
login_mode
==
MANUAL_LOGIN
\
or
(
not
password
and
not
private_key
):
prompt
=
"{}'s password: "
.
format
(
self
.
system_user
.
username
)
...
...
@@ -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
:
...
...
coco/recorder.py
View file @
7a7b265e
...
...
@@ -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
(
se
ssion_id
,
msg
,
times
se
lf
.
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
(
se
lf
.
se
ssion_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
...
...
coco/session.py
View file @
7a7b265e
...
...
@@ -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
...
...
coco/sftp.py
View file @
7a7b265e
...
...
@@ -45,7 +45,8 @@ def convert_error(func):
class
SFTPServer
(
paramiko
.
SFTPServerInterface
):
root
=
'/tmp'
# 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
):
"""
...
...
@@ -107,7 +108,7 @@ class SFTPServer(paramiko.SFTPServerInterface):
self
.
_sftp
=
{}
def
get_host_sftp
(
self
,
host
,
su
):
asset
=
self
.
hosts
.
get
(
host
)[
'asset'
]
asset
=
self
.
hosts
.
get
(
host
,
{})
.
get
(
'asset'
)
system_user
=
self
.
get_host_system_users
(
host
,
only_name
=
False
)
.
get
(
su
)
if
not
asset
or
not
system_user
:
...
...
@@ -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
...
...
coco/utils.py
View file @
7a7b265e
...
...
@@ -4,10 +4,13 @@
from
__future__
import
unicode_literals
import
time
import
logging
import
re
import
os
import
gettext
import
gzip
import
psutil
from
io
import
StringIO
from
binascii
import
hexlify
from
werkzeug.local
import
Local
,
LocalProxy
...
...
@@ -464,4 +467,73 @@ 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
)
def
get_cpu_info
():
cpus
=
[
'cpu
%
s'
%
i
for
i
in
range
(
psutil
.
cpu_count
())]
percents
=
psutil
.
cpu_percent
(
interval
=
0.5
,
percpu
=
True
)
return
dict
(
zip
(
cpus
,
percents
))
def
get_memory_info
():
mem
=
psutil
.
virtual_memory
()
return
{
'total'
:
mem
.
total
,
'avail'
:
mem
.
available
,
'used'
:
mem
.
used
,
'percent'
:
mem
.
percent
,
}
def
get_disk_info
():
partitions
=
psutil
.
disk_partitions
()
info
=
{}
for
partition
in
partitions
:
usage
=
psutil
.
disk_usage
(
partition
.
mountpoint
)
info
[
partition
.
device
]
=
{
'mountpoint'
:
partition
.
mountpoint
,
'device'
:
partition
.
device
,
'total'
:
usage
.
total
,
'used'
:
usage
.
used
,
'free'
:
usage
.
free
,
'percent'
:
usage
.
percent
}
return
info
def
get_net_info
():
counter
=
psutil
.
net_io_counters
()
return
{
'bytes_sent'
:
counter
.
bytes_sent
,
'bytes_recv'
:
counter
.
bytes_recv
,
}
def
get_coco_monitor_data
():
p
=
psutil
.
Process
(
os
.
getpid
())
cpu_used
=
p
.
cpu_percent
(
interval
=
0.5
)
memory_used
=
p
.
memory_info
()
.
rss
connections
=
len
(
p
.
connections
())
return
{
'p_cpu'
:
cpu_used
,
'p_memory'
:
memory_used
,
'p_conns'
:
connections
}
def
get_monitor_data
():
return
{
'timestamp'
:
int
(
time
.
time
()),
'cpu'
:
get_cpu_info
(),
'memory'
:
get_memory_info
(),
'disk'
:
get_disk_info
(),
'net'
:
get_net_info
(),
}
ugettext
=
LocalProxy
(
partial
(
_find
,
'LANGUAGE_CODE'
))
cocod
View file @
7a7b265e
...
...
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
#
import
os
import
subprocess
if
os
.
environ
.
get
(
'USE_EVENTLET'
,
'1'
)
==
'1'
:
import
eventlet
...
...
@@ -17,12 +18,30 @@ import argparse
import
time
import
signal
dirs
=
(
'logs'
,
'keys'
)
BASE_DIR
=
os
.
path
.
abspath
(
os
.
path
.
dirname
(
__file__
))
sys
.
path
.
insert
(
0
,
BASE_DIR
)
dirs
=
(
'logs'
,)
for
d
in
dirs
:
if
not
os
.
path
.
isdir
(
d
):
os
.
makedirs
(
d
)
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
...
...
conf_docker.py
deleted
100644 → 0
View file @
8db5dd32
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import
os
BASE_DIR
=
os
.
path
.
dirname
(
__file__
)
class
Config
:
"""
Coco config file
"""
# 默认的名字
NAME
=
os
.
environ
.
get
(
"NAME"
)
or
None
# Jumpserver项目的url, api请求注册会使用
CORE_HOST
=
os
.
environ
.
get
(
"CORE_HOST"
)
or
'http://core:8080'
# 启动时绑定的ip, 默认 0.0.0.0
BIND_HOST
=
'0.0.0.0'
# 监听的SSH端口号, 默认2222
SSHD_PORT
=
2222
# 监听的HTTP/WS端口号,默认5000
HTTPD_PORT
=
5000
# 项目使用的ACCESS KEY, 默认会注册,并保存到 ACCESS_KEY_STORE中,
# 如果有需求, 可以写到配置文件中, 格式 access_key_id:access_key_secret
ACCESS_KEY
=
os
.
environ
.
get
(
"ACCESS_KEY"
)
or
None
# ACCESS KEY 保存的地址, 默认注册后会保存到该文件中
# ACCESS_KEY_STORE = os.path.join(BASE_DIR, 'keys', '.access_key')
# 加密密钥
SECRET_KEY
=
os
.
environ
.
get
(
"SECRET_KEY"
)
or
'SKdfm239LSKdfj())_23jK*^2'
# 设置日志级别 ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'CRITICAL']
LOG_LEVEL
=
os
.
environ
.
get
(
"LOG_LEVEL"
)
or
'DEBUG'
# 日志存放的目录
LOG_DIR
=
os
.
environ
.
get
(
"LOG_DIR"
)
or
os
.
path
.
join
(
BASE_DIR
,
'logs'
)
# Session录像存放目录
SESSION_DIR
=
os
.
environ
.
get
(
"SESSION_DIR"
)
or
os
.
path
.
join
(
BASE_DIR
,
'sessions'
)
# 资产显示排序方式, ['ip', 'hostname']
ASSET_LIST_SORT_BY
=
os
.
environ
.
get
(
"SESSION_DIR"
)
or
'ip'
# 登录是否支持密码认证
SSH_PASSWORD_AUTH
=
bool
(
os
.
environ
.
get
(
"SSH_PASSWORD_AUTH"
))
if
os
.
environ
.
get
(
"SSH_PASSWORD_AUTH"
)
else
True
# 登录是否支持秘钥认证
SSH_PUBLIC_KEY_AUTH
=
bool
(
os
.
environ
.
get
(
"SSH_PUBLIC_KEY_AUTH"
))
if
os
.
environ
.
get
(
"SSH_PUBLIC_KEY_AUTH"
)
else
True
# 和Jumpserver 保持心跳时间间隔
HEARTBEAT_INTERVAL
=
int
(
os
.
environ
.
get
(
"HEARTBEAT_INTERVAL"
))
if
os
.
environ
.
get
(
"HEARTBEAT_INTERVAL"
)
else
5
# Admin的名字,出问题会提示给用户
ADMINS
=
os
.
environ
.
get
(
"ADMINS"
)
or
''
COMMAND_STORAGE
=
{
"TYPE"
:
"server"
}
class
ConfigDocker
(
Config
):
pass
config
=
ConfigDocker
()
\ No newline at end of file
config_example.yml
View file @
7a7b265e
...
...
@@ -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
entrypoint.sh
0 → 100755
View file @
7a7b265e
#!/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
locale/en/LC_MESSAGES/coco.mo
View file @
7a7b265e
No preview for this file type
locale/en/LC_MESSAGES/coco.po
View file @
7a7b265e
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 201
8-12-21 16:48
+0800\n"
"POT-Creation-Date: 201
9-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:1
45
#: coco/app.py:1
82
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:8
6
#: coco/interactive.py:8
9
#, 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:9
0
#: coco/interactive.py:9
3
#, 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:9
1
#: coco/interactive.py:9
4
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr ""
#: coco/interactive.py:9
2
#: coco/interactive.py:9
5
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr ""
#: coco/interactive.py:9
3
#: coco/interactive.py:9
6
#, python-brace-format
msgid "{T}8) Enter {green}r{end} to refresh your assets and nodes.{R}"
msgstr ""
#: coco/interactive.py:9
4
#: coco/interactive.py:9
7
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr ""
#: coco/interactive.py:15
5
#: coco/interactive.py:15
8
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:2
80
msgid "
Tips: Enter the asset ID and log directly into the asset.
"
#: coco/interactive.py:2
66
msgid "
ID
"
msgstr ""
#: coco/interactive.py:2
81
msgid "
Page up: P/p
"
#: coco/interactive.py:2
66
msgid "
Hostname
"
msgstr ""
#: coco/interactive.py:2
82
msgid "
Page down: Enter|N/n
"
#: coco/interactive.py:2
66
msgid "
IP
"
msgstr ""
#: coco/interactive.py:2
83
msgid "
BACK: b/q
"
#: coco/interactive.py:2
66
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:3
26
msgid "
Page: {}, Count: {}, Total Page: {}, Total Count: {}
"
#: coco/interactive.py:3
00
msgid "
BACK: b/q
"
msgstr ""
#: coco/interactive.py:3
98
#: coco/interactive.py:3
71
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:4
42
#: coco/interactive.py:4
15
msgid "Select a login:: "
msgstr ""
#: coco/interactive.py:4
65
#: coco/interactive.py:4
38
msgid "No system user"
msgstr ""
#: coco/models.py:2
4
2
#: coco/models.py:2
5
2
msgid ""
"Warning: Failed to load filter rule, please press Ctrl + D to exit retry."
msgstr ""
#: coco/models.py:2
5
1
#: coco/models.py:2
6
1
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:1
31
#: coco/proxy.py:1
47
msgid "Connecting to {}@{} {:.1f}"
msgstr ""
#: coco/session.py:1
43
#: coco/session.py:1
54
msgid "Terminated by administrator"
msgstr ""
locale/zh_CN/LC_MESSAGES/coco.mo
View file @
7a7b265e
No preview for this file type
locale/zh_CN/LC_MESSAGES/coco.po
View file @
7a7b265e
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 201
8-12-21 16:48
+0800\n"
"POT-Creation-Date: 201
9-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:1
45
#: coco/app.py:1
82
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:8
6
#: coco/interactive.py:8
9
#, 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:9
0
#: coco/interactive.py:9
3
#, 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:9
1
#: coco/interactive.py:9
4
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}"
#: coco/interactive.py:9
2
#: coco/interactive.py:9
5
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:9
3
#: coco/interactive.py:9
6
#, 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:9
4
#: coco/interactive.py:9
7
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}"
#: coco/interactive.py:15
5
#: coco/interactive.py:15
8
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:4
42
#: coco/interactive.py:4
15
msgid "Select a login:: "
msgstr "选择一个登录:"
#: coco/interactive.py:4
65
#: coco/interactive.py:4
38
msgid "No system user"
msgstr "没有系统用户"
#: coco/models.py:2
4
2
#: coco/models.py:2
5
2
msgid ""
"Warning: Failed to load filter rule, please press Ctrl + D to exit retry."
msgstr "警告: 加载过滤规则失败,请按 Ctrl + D 退出重试."
#: coco/models.py:2
5
1
#: coco/models.py:2
6
1
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:1
31
#: coco/proxy.py:1
47
msgid "Connecting to {}@{} {:.1f}"
msgstr "开始连接到 {}@{} {:.1f}"
#: coco/session.py:1
43
#: coco/session.py:1
54
msgid "Terminated by administrator"
msgstr "被管理员中断"
...
...
requirements/requirements.txt
View file @
7a7b265e
...
...
@@ -4,7 +4,7 @@ boto3==1.6.5
botocore==1.9.5
cachetools==2.0.1
certifi==2018.1.18
cffi==1.11.
2
cffi==1.11.
5
chardet==3.0.4
click==6.7
crcmod==1.7
...
...
@@ -18,14 +18,14 @@ idna==2.6
itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
jms-storage==0.0.2
0
jumpserver-python-sdk==0.0.5
5
jms-storage==0.0.2
2
jumpserver-python-sdk==0.0.5
6
MarkupSafe==1.0
oss2==2.4.0
paramiko==2.4.1
psutil==5.4.1
pyasn1==0.4.2
pycparser==2.1
8
pycparser==2.1
9
PyNaCl==1.2.1
pyte==0.8.0
python-dateutil==2.6.1
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment