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
6e153a1a
Commit
6e153a1a
authored
Dec 24, 2018
by
ibuler
Browse files
Options
Browse Files
Download
Plain Diff
[Update] youhua
parents
9d64f6c2
9c24dcb0
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
457 additions
and
385 deletions
+457
-385
app.py
coco/app.py
+11
-1
config.py
coco/config.py
+19
-1
connection.py
coco/connection.py
+8
-4
connector.py
coco/httpd/elfinder/connector.py
+1
-0
interactive.py
coco/interactive.py
+285
-252
interface.py
coco/interface.py
+1
-1
logger.py
coco/logger.py
+1
-1
models.py
coco/models.py
+5
-0
proxy.py
coco/proxy.py
+4
-2
sshd.py
coco/sshd.py
+1
-0
utils.py
coco/utils.py
+2
-12
conf_example.py
conf_example.py
+4
-0
coco.mo
locale/en/LC_MESSAGES/coco.mo
+0
-0
coco.po
locale/en/LC_MESSAGES/coco.po
+44
-48
coco.mo
locale/zh_CN/LC_MESSAGES/coco.mo
+0
-0
coco.po
locale/zh_CN/LC_MESSAGES/coco.po
+69
-62
alpine_requirements.txt
requirements/alpine_requirements.txt
+1
-0
requirements.txt
requirements/requirements.txt
+1
-1
No files found.
coco/app.py
View file @
6e153a1a
...
...
@@ -20,7 +20,7 @@ from .session import Session
from
.models
import
Connection
__version__
=
'1.4.
4
'
__version__
=
'1.4.
6
'
BASE_DIR
=
os
.
path
.
dirname
(
os
.
path
.
dirname
(
__file__
))
logger
=
get_logger
(
__file__
)
...
...
@@ -56,6 +56,7 @@ class Coco:
return
self
.
_task_handler
@staticmethod
@ignore_error
def
load_extra_conf_from_server
():
configs
=
app_service
.
load_config_from_server
()
logger
.
debug
(
"Loading config from server: {}"
.
format
(
...
...
@@ -63,8 +64,17 @@ class Coco:
))
config
.
update
(
configs
)
def
keep_load_extra_conf
(
self
):
def
func
():
while
True
:
self
.
load_extra_conf_from_server
()
time
.
sleep
(
60
*
10
)
thread
=
threading
.
Thread
(
target
=
func
)
thread
.
start
()
def
bootstrap
(
self
):
self
.
load_extra_conf_from_server
()
self
.
keep_load_extra_conf
()
self
.
keep_heartbeat
()
self
.
monitor_sessions
()
self
.
monitor_sessions_replay
()
...
...
coco/config.py
View file @
6e153a1a
...
...
@@ -92,8 +92,9 @@ class Config(dict):
"""
def
__init__
(
self
,
root_path
,
defaults
=
None
):
dict
.
__init__
(
self
,
defaults
or
{})
self
.
defaults
=
defaults
or
{}
self
.
root_path
=
root_path
super
()
.
__init__
({})
def
from_envvar
(
self
,
variable_name
,
silent
=
False
):
"""Loads a configuration from an environment variable pointing to
...
...
@@ -269,6 +270,21 @@ class Config(dict):
rv
[
key
]
=
v
return
rv
def
__getitem__
(
self
,
item
):
try
:
value
=
super
()
.
__getitem__
(
item
)
except
KeyError
:
value
=
None
if
value
is
not
None
:
return
value
value
=
os
.
environ
.
get
(
item
,
None
)
if
value
is
not
None
:
return
value
return
self
.
defaults
.
get
(
item
)
def
__getattr__
(
self
,
item
):
return
self
.
__getitem__
(
item
)
def
__repr__
(
self
):
return
'<
%
s
%
s>'
%
(
self
.
__class__
.
__name__
,
dict
.
__repr__
(
self
))
...
...
@@ -277,6 +293,7 @@ access_key_path = os.path.abspath(os.path.join(root_path, 'keys', '.access_key')
default_config
=
{
'NAME'
:
socket
.
gethostname
(),
'CORE_HOST'
:
'http://127.0.0.1:8080'
,
'BOOTSTRAP_TOKEN'
:
os
.
environ
.
get
(
"BOOTSTRAP_TOKEN"
)
or
'PleaseChangeMe'
,
'ROOT_PATH'
:
root_path
,
'DEBUG'
:
True
,
'BIND_HOST'
:
'0.0.0.0'
,
...
...
@@ -301,6 +318,7 @@ default_config = {
'REPLAY_STORAGE'
:
{
'TYPE'
:
'server'
},
'LANGUAGE_CODE'
:
'zh'
,
'SECURITY_MAX_IDLE_TIME'
:
60
,
'ASSET_LIST_PAGE_SIZE'
:
'auto'
,
}
config
=
Config
(
root_path
,
default_config
)
...
...
coco/connection.py
View file @
6e153a1a
...
...
@@ -41,12 +41,16 @@ class SSHConnection:
ssh
=
paramiko
.
SSHClient
()
ssh
.
set_missing_host_key_policy
(
paramiko
.
AutoAddPolicy
())
sock
=
None
error
=
''
if
not
system_user
.
password
and
not
system_user
.
private_key
:
self
.
get_system_user_auth
(
system_user
)
if
asset
.
domain
:
sock
=
self
.
get_proxy_sock_v2
(
asset
)
if
not
sock
:
error
=
'Connect gateway failed;'
logger
.
error
(
error
)
try
:
try
:
...
...
@@ -85,9 +89,9 @@ class SSHConnection:
system_user
.
username
,
asset
.
ip
,
asset
.
port
,
password_short
,
key_fingerprint
,
))
return
None
,
None
,
str
(
e
)
return
None
,
None
,
error
+
'
\n
'
+
str
(
e
)
except
(
socket
.
error
,
socket
.
timeout
)
as
e
:
return
None
,
None
,
str
(
e
)
return
None
,
None
,
error
+
'
\n
'
+
str
(
e
)
return
ssh
,
sock
,
None
def
get_transport
(
self
,
asset
,
system_user
):
...
...
@@ -130,9 +134,9 @@ class SSHConnection:
password
=
gateway
.
password
,
pkey
=
gateway
.
private_key_obj
,
timeout
=
config
[
'SSH_TIMEOUT'
])
except
(
paramiko
.
AuthenticationException
,
except
(
paramiko
.
AuthenticationException
,
paramiko
.
BadAuthenticationType
,
SSHException
):
SSHException
,
socket
.
error
):
continue
sock
=
ssh
.
get_transport
()
.
open_channel
(
'direct-tcpip'
,
(
asset
.
ip
,
asset
.
port
),
(
'127.0.0.1'
,
0
)
...
...
coco/httpd/elfinder/connector.py
View file @
6e153a1a
...
...
@@ -136,6 +136,7 @@ class ElFinderConnector:
return
cmd
else
:
self
.
response
[
'error'
]
=
'No valid command found'
return
None
,
None
def
run
(
self
,
request
):
""" Main entry point for running commands. Attemps to run a command
...
...
coco/interactive.py
View file @
6e153a1a
...
...
@@ -33,53 +33,57 @@ class InteractiveServer:
def
__init__
(
self
,
client
):
self
.
client
=
client
self
.
closed
=
False
self
.
_
search_result
=
None
self
.
_
results
=
None
self
.
nodes
=
None
self
.
offset
=
0
self
.
limit
=
100
self
.
assets
_list
=
[]
self
.
assets
=
[]
self
.
finish
=
False
self
.
page
=
1
self
.
total_assets
=
0
self
.
total_count
=
0
# 分页展示中
用来存放数目总条数
self
.
node
s
_tree
=
None
# 授权节点树
self
.
get_user_assets_
paging_
async
()
self
.
total_assets
=
0
# 用户被授权的所有资产
self
.
total_count
=
0
# 分页展示中
的资产总数量
self
.
node_tree
=
None
# 授权节点树
self
.
get_user_assets_async
()
self
.
get_user_nodes_async
()
@property
def
page_size
(
self
):
_page_size
=
config
[
'ASSET_LIST_PAGE_SIZE'
]
if
_page_size
.
isdigit
():
return
int
(
_page_size
)
elif
_page_size
==
'all'
:
return
self
.
total_count
else
:
return
self
.
client
.
request
.
meta
[
'height'
]
-
8
@property
def
search_result
(
self
):
if
self
.
_search_result
:
return
self
.
_search_result
def
total_pages
(
self
):
return
math
.
ceil
(
self
.
total_count
/
self
.
page_size
)
@property
def
need_paging
(
self
):
return
config
[
'ASSET_LIST_PAGE_SIZE'
]
!=
'all'
@property
def
results
(
self
):
if
self
.
_results
:
return
self
.
_results
else
:
return
[]
@search_result.setter
def
search_result
(
self
,
value
):
if
not
value
:
self
.
_search_result
=
value
return
value
=
self
.
filter_system_users
(
value
)
self
.
_search_result
=
value
@results.setter
def
results
(
self
,
value
):
self
.
_results
=
value
def
display_logo
(
self
):
logo_path
=
os
.
path
.
join
(
config
[
'ROOT_PATH'
],
"logo.txt"
)
if
not
os
.
path
.
isfile
(
logo_path
):
return
with
open
(
logo_path
,
'rb'
)
as
f
:
for
i
in
f
:
if
i
.
decode
(
'utf-8'
)
.
startswith
(
'#'
):
continue
self
.
client
.
send
(
i
.
decode
(
'utf-8'
)
.
replace
(
'
\n
'
,
'
\r\n
'
))
#
# Display banner
#
def
display_banner
(
self
):
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}"
)
menu
s
=
[
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}"
),
_
(
"{T}3) Enter {green}p{end} to display the host you have permission.{R}"
),
...
...
@@ -87,27 +91,38 @@ class InteractiveServer:
_
(
"{T}5) Enter {green}g{end} + {green}NodeID{end} to display the host under the node, such as g1.{R}"
),
_
(
"{T}6) Enter {green}s{end} Chinese-english switch.{R}"
),
_
(
"{T}7) Enter {green}h{end} help.{R}"
),
_
(
"{T}8) Enter {green}r{end} to refresh your assets and nodes.{R}"
),
_
(
"{T}0) Enter {green}q{end} exit.{R}"
)
]
self
.
client
.
send
(
header
.
format
(
title
=
"
\033
[1;32m"
,
user
=
self
.
client
.
user
,
end
=
"
\033
[0m"
,
T
=
'
\t
'
,
R
=
'
\r\n\r
'
))
for
menu
in
menus
:
self
.
client
.
send
(
menu
.
format
(
for
item
in
menu
:
self
.
client
.
send
(
item
.
format
(
green
=
"
\033
[32m"
,
end
=
"
\033
[0m"
,
T
=
'
\t
'
,
R
=
'
\r\n\r
'
))
def
display_logo
(
self
):
logo_path
=
os
.
path
.
join
(
config
[
'ROOT_PATH'
],
"logo.txt"
)
if
not
os
.
path
.
isfile
(
logo_path
):
return
with
open
(
logo_path
,
'rb'
)
as
f
:
for
i
in
f
:
if
i
.
decode
(
'utf-8'
)
.
startswith
(
'#'
):
continue
self
.
client
.
send
(
i
.
decode
(
'utf-8'
)
.
replace
(
'
\n
'
,
'
\r\n
'
))
def
dispatch
(
self
,
opt
):
if
opt
is
None
:
return
self
.
_sentinel
elif
opt
.
startswith
(
"/"
):
self
.
search_and_display
(
opt
.
lstrip
(
"/"
))
self
.
search_and_display
_assets
(
opt
.
lstrip
(
"/"
))
elif
opt
in
[
'p'
,
'P'
,
''
]:
self
.
display_assets
()
elif
opt
in
[
'g'
,
'G'
]:
self
.
display_nodes_tree
()
self
.
display_nodes_
as_
tree
()
elif
opt
.
startswith
(
"g"
)
and
opt
.
lstrip
(
"g"
)
.
isdigit
():
self
.
display_node_assets
(
int
(
opt
.
lstrip
(
"g"
)))
elif
opt
in
[
'q'
,
'Q'
,
'exit'
,
'quit'
]:
...
...
@@ -115,97 +130,155 @@ class InteractiveServer:
elif
opt
in
[
's'
,
'S'
]:
switch_lang
()
self
.
display_banner
()
elif
opt
in
[
'r'
,
'R'
]:
self
.
refresh_assets_nodes
()
self
.
display_banner
()
elif
opt
in
[
'h'
,
'H'
]:
self
.
display_banner
()
else
:
self
.
search_and_proxy
(
opt
)
self
.
search_and_proxy_assets
(
opt
)
#
# Search assets
#
def
search_and_display_assets
(
self
,
q
):
assets
=
self
.
search_assets
(
q
)
self
.
display_assets
(
assets
)
def
search_and_proxy_assets
(
self
,
opt
):
assets
=
self
.
search_assets
(
opt
)
if
assets
and
len
(
assets
)
==
1
:
asset
=
assets
[
0
]
if
asset
.
protocol
==
"rdp"
\
or
asset
.
platform
.
lower
()
.
startswith
(
"windows"
):
self
.
client
.
send
(
warning
(
_
(
"Terminal does not support login rdp, "
"please use web terminal to access"
))
)
return
self
.
proxy
(
asset
)
else
:
self
.
display_assets
(
assets
)
def
refresh_assets_nodes
(
self
):
self
.
get_user_assets_async
()
self
.
get_user_nodes_async
()
def
search_assets
(
self
,
q
):
if
not
self
.
finish
:
if
self
.
finish
:
assets
=
self
.
search_assets_from_local
(
q
)
else
:
assets
=
self
.
search_assets_from_server
(
q
)
return
assets
def
search_assets_from_server
(
self
,
q
):
assets
=
app_service
.
get_search_user_granted_assets
(
self
.
client
.
user
,
q
)
assets
=
self
.
filter_system_users
(
assets
)
return
assets
assets
=
self
.
assets_list
def
search_assets_from_local
(
self
,
q
):
result
=
[]
# 所有的
if
q
in
(
''
,
None
):
result
=
assets
result
=
self
.
assets
# 用户输入的是数字,可能想使用id唯一键搜索
elif
q
.
isdigit
()
and
self
.
results
and
\
len
(
self
.
results
)
>=
int
(
q
):
result
=
[
self
.
results
[
int
(
q
)
-
1
]]
# 全匹配到则直接返回全匹配的
if
len
(
result
)
==
0
:
_result
=
[
asset
for
asset
in
assets
if
is_obj_attr_eq
(
asset
,
q
)]
_result
=
[
asset
for
asset
in
self
.
assets
if
is_obj_attr_eq
(
asset
,
q
)]
if
len
(
_result
)
==
1
:
result
=
_result
# 最后模糊匹配
if
len
(
result
)
==
0
:
result
=
[
asset
for
asset
in
assets
if
is_obj_attr_has
(
asset
,
q
)]
result
=
[
asset
for
asset
in
self
.
assets
if
is_obj_attr_has
(
asset
,
q
)]
return
result
def
display_assets
(
self
):
"""
Display user all assets
:return:
"""
self
.
display_result_paging
(
self
.
assets_list
)
#
# Display assets
#
def
display_nodes
(
self
):
if
self
.
nodes
is
None
:
self
.
get_user_nodes
()
if
len
(
self
.
nodes
)
==
0
:
self
.
client
.
send
(
warning
(
_
(
"No"
)))
return
id_length
=
max
(
len
(
str
(
len
(
self
.
nodes
))),
5
)
name_length
=
item_max_length
(
self
.
nodes
,
15
,
key
=
lambda
x
:
x
.
name
)
amount_length
=
item_max_length
(
self
.
nodes
,
10
,
key
=
lambda
x
:
x
.
assets_amount
)
size_list
=
[
id_length
,
name_length
,
amount_length
]
fake_data
=
[
'ID'
,
_
(
"Name"
),
_
(
"Assets"
)]
def
display_assets
(
self
,
assets
=
None
):
if
assets
is
None
:
while
not
self
.
assets
and
not
self
.
finish
:
time
.
sleep
(
0.2
)
assets
=
self
.
assets
self
.
display_assets_paging
(
assets
)
self
.
client
.
send
(
wr
(
title
(
format_with_zh
(
size_list
,
*
fake_data
))))
for
index
,
node
in
enumerate
(
self
.
nodes
,
1
):
data
=
[
index
,
node
.
name
,
node
.
assets_amount
]
self
.
client
.
send
(
wr
(
format_with_zh
(
size_list
,
*
data
)))
self
.
client
.
send
(
wr
(
_
(
"Total: {}"
)
.
format
(
len
(
self
.
nodes
)),
before
=
1
))
def
display_nodes_tree
(
self
):
if
self
.
nodes
is
None
:
self
.
get_user_nodes
()
if
not
self
.
nodes
:
self
.
client
.
send
(
wr
(
_
(
'No Nodes'
),
before
=
0
))
def
display_assets_paging
(
self
,
assets
):
if
len
(
assets
)
==
0
:
self
.
client
.
send
(
wr
(
_
(
"No Assets"
),
before
=
0
))
return
self
.
total_count
=
self
.
total_assets
if
assets
is
self
.
assets
else
len
(
assets
)
self
.
nodes_tree
.
show
(
key
=
lambda
node
:
node
.
identifier
)
self
.
client
.
send
(
wr
(
title
(
_
(
"Node: [ ID.Name(Asset amount) ]"
)),
before
=
0
))
self
.
client
.
send
(
wr
(
self
.
nodes_tree
.
_reader
.
replace
(
'
\n
'
,
'
\r\n
'
),
before
=
0
))
prompt
=
_
(
"Tips: Enter g+NodeID to display the host under the node, such as g1"
)
self
.
client
.
send
(
wr
(
title
(
prompt
),
before
=
1
))
action
=
None
gen
=
self
.
_page_generator
(
assets
)
while
True
:
try
:
page
,
_assets
=
gen
.
send
(
action
)
except
StopIteration
as
e
:
if
None
not
in
e
.
value
:
page
,
_assets
=
e
.
value
self
.
display_a_page_assets
(
page
,
_assets
)
break
else
:
self
.
display_a_page_assets
(
page
,
_assets
)
self
.
display_page_bottom_prompt
()
action
=
self
.
get_user_action
()
def
display_node_assets
(
self
,
_id
):
if
self
.
nodes
is
None
:
self
.
get_user_nodes
()
if
_id
>
len
(
self
.
nodes
)
or
_id
<=
0
:
msg
=
wr
(
warning
(
_
(
"There is no matched node, please re-enter"
)))
self
.
client
.
send
(
msg
)
self
.
display_nodes_tree
()
return
def
_page_generator
(
self
,
assets
):
start
,
page
=
0
,
1
while
True
:
_assets
=
assets
[
start
:
start
+
self
.
page_size
]
# 等待加载
if
(
assets
is
self
.
assets
)
and
(
not
self
.
finish
)
and
(
not
self
.
need_paging
):
time
.
sleep
(
1
)
continue
# 最后一页
elif
_assets
and
(
page
==
self
.
total_pages
)
and
(
assets
is
not
self
.
assets
or
(
assets
is
self
.
assets
and
self
.
finish
)):
return
page
,
_assets
# 执行动作
else
:
action
=
yield
page
,
_assets
assets
=
self
.
nodes
[
_id
-
1
]
.
assets_granted
self
.
display_result_paging
(
assets
)
# 退出
if
action
==
BACK
:
return
None
,
None
# 不分页, 不对页码和下标做更改
elif
not
self
.
need_paging
:
continue
# 上一页
elif
action
==
PAGE_UP
:
if
page
<=
1
:
page
=
1
start
=
0
else
:
page
-=
1
start
-=
self
.
page_size
# 下一页
else
:
page
+=
1
start
+=
len
(
_assets
)
def
display_search_result
(
self
):
def
display_a_page_assets
(
self
,
page
,
assets
):
self
.
client
.
send
(
char
.
CLEAR_CHAR
)
self
.
page
=
page
sort_by
=
config
[
"ASSET_LIST_SORT_BY"
]
self
.
search_result
=
sort_assets
(
self
.
search_result
,
sort_by
)
self
.
results
=
sort_assets
(
assets
,
sort_by
)
fake_data
=
[
_
(
"ID"
),
_
(
"Hostname"
),
_
(
"IP"
),
_
(
"LoginAs"
)]
id_length
=
max
(
len
(
str
(
len
(
self
.
search_result
))),
4
)
hostname_length
=
item_max_length
(
self
.
search_result
,
15
,
id_length
=
max
(
len
(
str
(
len
(
self
.
results
))),
4
)
hostname_length
=
item_max_length
(
self
.
results
,
15
,
key
=
lambda
x
:
x
.
hostname
)
sysuser_length
=
item_max_length
(
self
.
search_result
,
sysuser_length
=
item_max_length
(
self
.
results
,
key
=
lambda
x
:
x
.
system_users_name_list
)
size_list
=
[
id_length
,
hostname_length
,
16
,
sysuser_length
]
header_without_comment
=
format_with_zh
(
size_list
,
*
fake_data
)
...
...
@@ -217,43 +290,134 @@ class InteractiveServer:
size_list
.
append
(
comment_length
)
fake_data
.
append
(
_
(
"Comment"
))
self
.
client
.
send
(
wr
(
title
(
format_with_zh
(
size_list
,
*
fake_data
))))
for
index
,
asset
in
enumerate
(
self
.
search_result
,
1
):
for
index
,
asset
in
enumerate
(
self
.
results
,
1
):
data
=
[
index
,
asset
.
hostname
,
asset
.
ip
,
asset
.
system_users_name_list
,
asset
.
comment
]
self
.
client
.
send
(
wr
(
format_with_zh
(
size_list
,
*
data
)))
total_page
=
math
.
ceil
(
self
.
total_count
/
self
.
page_size
)
self
.
client
.
send
(
wr
(
title
(
_
(
"Page: {}, Count: {}, Total Page: {}, Total Count: {}"
)
.
format
(
self
.
page
,
len
(
self
.
search_result
),
total_page
,
self
.
total_count
)),
before
=
1
)
self
.
client
.
send
(
wr
(
title
(
_
(
"Page: {}, Count: {}, Total Page: {}, Total Count: {}"
)
.
format
(
self
.
page
,
len
(
self
.
results
),
self
.
total_pages
,
self
.
total_count
)),
before
=
1
)
)
def
search_and_display
(
self
,
q
):
assets
=
self
.
search_assets
(
q
)
self
.
display_result_paging
(
assets
)
def
display_page_bottom_prompt
(
self
):
msg
=
wr
(
_
(
'Tips: Enter the asset ID and log directly into the asset.'
))
self
.
client
.
send
(
msg
,
before
=
1
)
prompt_page_up
=
_
(
"Page up: P/p"
)
prompt_page_down
=
_
(
"Page down: Enter|N/n"
)
prompt_back
=
_
(
"BACK: b/q"
)
prompts
=
[
prompt_page_up
,
prompt_page_down
,
prompt_back
]
prompt
=
'
\t
'
.
join
(
prompts
)
self
.
client
.
send
(
wr
(
prompt
,
before
=
1
))
def
get_user_nodes
(
self
):
self
.
nodes
=
app_service
.
get_user_asset_groups
(
self
.
client
.
user
)
self
.
sort_nodes
()
self
.
construct_nodes_tree
()
def
get_user_action
(
self
):
opt
=
net_input
(
self
.
client
,
prompt
=
':'
)
if
opt
in
(
'p'
,
'P'
):
return
PAGE_UP
elif
opt
in
(
'b'
,
'q'
):
return
BACK
elif
opt
.
isdigit
()
and
self
.
results
and
0
<
int
(
opt
)
<=
len
(
self
.
results
):
self
.
proxy
(
self
.
results
[
int
(
opt
)
-
1
])
return
BACK
else
:
return
PAGE_DOWN
#
# Get assets
#
def
get_user_assets_async
(
self
):
if
self
.
need_paging
:
thread
=
threading
.
Thread
(
target
=
self
.
get_user_assets_paging
)
else
:
thread
=
threading
.
Thread
(
target
=
self
.
get_user_assets_direct
)
thread
.
start
()
def
get_user_assets_direct
(
self
):
assets
=
app_service
.
get_user_assets
(
self
.
client
.
user
)
assets
=
self
.
filter_system_users
(
assets
)
self
.
assets
=
assets
self
.
total_assets
=
len
(
assets
)
self
.
finish
=
True
def
get_user_assets_paging
(
self
):
while
not
self
.
closed
:
assets
,
total
=
app_service
.
get_user_assets_paging
(
self
.
client
.
user
,
offset
=
self
.
offset
,
limit
=
self
.
limit
)
if
not
assets
:
logger
.
info
(
'Get user assets paging async finished.'
)
self
.
finish
=
True
break
logger
.
info
(
'Get user assets paging async: {}'
.
format
(
len
(
assets
)))
assets
=
self
.
filter_system_users
(
assets
)
self
.
total_assets
=
total
self
.
assets
.
extend
(
assets
)
self
.
offset
+=
self
.
limit
#
# Nodes
#
def
sort_nodes
(
self
):
self
.
nodes
=
sorted
(
self
.
nodes
,
key
=
lambda
node
:
node
.
key
)
def
get_user_nodes_async
(
self
):
thread
=
threading
.
Thread
(
target
=
self
.
get_user_nodes
)
thread
.
start
()
def
construct_nodes_tree
(
self
):
self
.
nodes_tree
=
Tree
()
def
get_user_nodes
(
self
):
nodes
=
app_service
.
get_user_asset_groups
(
self
.
client
.
user
)
nodes
=
sorted
(
nodes
,
key
=
lambda
node
:
node
.
key
)
self
.
nodes
=
self
.
filter_system_users_of_assets_under_nodes
(
nodes
)
self
.
_construct_node_tree
()
def
filter_system_users_of_assets_under_nodes
(
self
,
nodes
):
for
node
in
nodes
:
node
.
assets_granted
=
self
.
filter_system_users
(
node
.
assets_granted
)
return
nodes
def
_construct_node_tree
(
self
):
self
.
node_tree
=
Tree
()
root
=
'ROOT_ALL_ORG_NODE'
self
.
node
s
_tree
.
create_node
(
tag
=
''
,
identifier
=
root
,
parent
=
None
)
self
.
node_tree
.
create_node
(
tag
=
''
,
identifier
=
root
,
parent
=
None
)
for
index
,
node
in
enumerate
(
self
.
nodes
):
tag
=
"{}.{}({})"
.
format
(
index
+
1
,
node
.
name
,
node
.
assets_amount
)
key
=
node
.
key
parent_key
=
key
[:
node
.
key
.
rfind
(
':'
)]
or
root
self
.
node
s
_tree
.
create_node
(
tag
=
tag
,
identifier
=
key
,
data
=
node
,
parent
=
parent_key
)
self
.
node_tree
.
create_node
(
tag
=
tag
,
identifier
=
key
,
data
=
node
,
parent
=
parent_key
)
def
get_user_nodes_async
(
self
):
thread
=
threading
.
Thread
(
target
=
self
.
get_user_nodes
)
thread
.
start
()
def
display_nodes_as_tree
(
self
):
if
self
.
nodes
is
None
:
self
.
get_user_nodes
()
if
not
self
.
nodes
:
self
.
client
.
send
(
wr
(
_
(
'No Nodes'
),
before
=
0
))
return
self
.
node_tree
.
show
(
key
=
lambda
node
:
node
.
identifier
)
self
.
client
.
send
(
wr
(
title
(
_
(
"Node: [ ID.Name(Asset amount) ]"
)),
before
=
0
))
self
.
client
.
send
(
wr
(
self
.
node_tree
.
_reader
.
replace
(
'
\n
'
,
'
\r\n
'
),
before
=
0
))
prompt
=
_
(
"Tips: Enter g+NodeID to display the host under the node, such as g1"
)
self
.
client
.
send
(
wr
(
title
(
prompt
),
before
=
1
))
def
display_node_assets
(
self
,
_id
):
if
self
.
nodes
is
None
:
self
.
get_user_nodes
()
if
_id
>
len
(
self
.
nodes
)
or
_id
<=
0
:
msg
=
wr
(
warning
(
_
(
"There is no matched node, please re-enter"
)))
self
.
client
.
send
(
msg
)
self
.
display_nodes_as_tree
()
return
assets
=
self
.
nodes
[
_id
-
1
]
.
assets_granted
self
.
display_assets
(
assets
)
#
# System users
#
@staticmethod
def
filter_system_users
(
assets
):
...
...
@@ -266,26 +430,6 @@ class InteractiveServer:
asset
.
system_users_granted
=
system_users_cleaned
return
assets
def
get_user_assets_paging
(
self
):
while
not
self
.
closed
:
assets
,
total
=
app_service
.
get_user_assets_paging
(
self
.
client
.
user
,
offset
=
self
.
offset
,
limit
=
self
.
limit
)
logger
.
debug
(
'Get user assets paging async: {}'
.
format
(
len
(
assets
)))
if
not
assets
:
logger
.
info
(
'Get user assets paging async finished.'
)
self
.
finish
=
True
return
if
not
self
.
total_assets
:
self
.
total_assets
=
total
self
.
total_count
=
total
self
.
assets_list
.
extend
(
assets
)
self
.
offset
+=
self
.
limit
def
get_user_assets_paging_async
(
self
):
thread
=
threading
.
Thread
(
target
=
self
.
get_user_assets_paging
)
thread
.
start
()
def
choose_system_user
(
self
,
system_users
):
if
len
(
system_users
)
==
1
:
return
system_users
[
0
]
...
...
@@ -309,125 +453,9 @@ class InteractiveServer:
for
index
,
system_user
in
enumerate
(
system_users
):
self
.
client
.
send
(
wr
(
"{} {}"
.
format
(
index
,
system_user
.
name
)))
def
search_and_proxy
(
self
,
opt
):
assets
=
self
.
search_assets
(
opt
)
if
assets
and
len
(
assets
)
==
1
:
asset
=
assets
[
0
]
self
.
search_result
=
None
if
asset
.
protocol
==
"rdp"
or
asset
.
platform
.
lower
()
.
startswith
(
"windows"
):
self
.
client
.
send
(
warning
(
_
(
"Terminal does not support login rdp, "
"please use web terminal to access"
))
)
return
self
.
proxy
(
asset
)
else
:
self
.
display_result_paging
(
assets
)
def
display_result_paging
(
self
,
result_list
):
if
result_list
is
self
.
assets_list
:
self
.
total_count
=
self
.
total_assets
else
:
if
len
(
result_list
)
==
0
:
return
self
.
total_count
=
len
(
result_list
)
action
=
PAGE_DOWN
gen_result
=
self
.
get_result_page_down_or_up
(
result_list
)
while
True
:
try
:
page
,
result
=
gen_result
.
send
(
action
)
except
TypeError
:
try
:
page
,
result
=
next
(
gen_result
)
except
StopIteration
:
logger
.
info
(
'No Assets'
)
# self.display_banner()
self
.
client
.
send
(
wr
(
_
(
"No Assets"
),
before
=
0
))
return
None
except
StopIteration
:
logger
.
debug
(
'Back display result paging.'
)
# self.display_banner()
return
None
self
.
display_result_of_page
(
page
,
result
)
action
=
self
.
get_user_action
()
def
get_result_page_down_or_up
(
self
,
result_list
):
left
=
0
page
=
1
page_up_size
=
0
# 记录上一页大小
while
True
:
right
=
left
+
self
.
page_size
result
=
result_list
[
left
:
right
]
if
not
result
and
(
result_list
is
self
.
assets_list
)
and
self
.
finish
and
self
.
total_assets
==
0
:
# 无授权资产
return
elif
not
result
and
(
result_list
is
self
.
assets_list
)
and
self
.
finish
:
# 上一页是最后一页
left
-=
page_up_size
page
-=
1
continue
elif
not
result
and
(
result_list
is
self
.
assets_list
)
and
not
self
.
finish
:
# 还有下一页(暂时没有加载完),需要等待
time
.
sleep
(
1
)
continue
elif
not
result
and
(
result_list
is
not
self
.
assets_list
):
# 上一页是最后一页
left
-=
page_up_size
page
-=
1
continue
else
:
# 其他4中情况,返回assets
action
=
yield
(
page
,
result
)
if
action
==
BACK
:
return
elif
action
==
PAGE_UP
:
if
page
<=
1
:
# 已经是第一页了
page
=
1
left
=
0
else
:
page
-=
1
left
-=
self
.
page_size
else
:
# PAGE_DOWN
page
+=
1
left
+=
len
(
result
)
page_up_size
=
len
(
result
)
def
display_result_of_page
(
self
,
page
,
result
):
self
.
client
.
send
(
char
.
CLEAR_CHAR
)
self
.
page
=
page
self
.
search_result
=
result
self
.
display_search_result
()
self
.
display_prompt_of_page
()
def
display_prompt_of_page
(
self
):
msg
=
wr
(
_
(
'Tips: Enter the asset ID and log directly into the asset.'
))
self
.
client
.
send
(
msg
,
before
=
1
)
prompt_page_up
=
_
(
"Page up: P/p"
)
prompt_page_down
=
_
(
"Page down: Enter|N/n"
)
prompt_back
=
_
(
"BACK: b/q"
)
prompts
=
[
prompt_page_up
,
prompt_page_down
,
prompt_back
]
prompt
=
'
\t
'
.
join
(
prompts
)
self
.
client
.
send
(
wr
(
prompt
,
before
=
1
))
def
get_user_action
(
self
):
opt
=
net_input
(
self
.
client
,
prompt
=
':'
)
if
opt
in
(
'p'
,
'P'
):
return
PAGE_UP
elif
opt
in
(
'b'
,
'q'
):
return
BACK
elif
opt
.
isdigit
()
and
self
.
search_result
and
0
<
int
(
opt
)
<=
len
(
self
.
search_result
):
self
.
proxy
(
self
.
search_result
[
int
(
opt
)
-
1
])
return
BACK
else
:
# PAGE_DOWN
return
PAGE_DOWN
#
# Proxy
#
def
proxy
(
self
,
asset
):
system_user
=
self
.
choose_system_user
(
asset
.
system_users_granted
)
...
...
@@ -437,6 +465,10 @@ class InteractiveServer:
forwarder
=
ProxyServer
(
self
.
client
,
asset
,
system_user
)
forwarder
.
proxy
()
#
# Entrance
#
def
interact
(
self
):
self
.
display_banner
()
while
not
self
.
closed
:
...
...
@@ -450,14 +482,15 @@ class InteractiveServer:
break
self
.
close
()
def
close
(
self
):
logger
.
debug
(
"Interactive server server close: {}"
.
format
(
self
))
self
.
closed
=
True
def
interact_async
(
self
):
# 目前没用
thread
=
threading
.
Thread
(
target
=
self
.
interact
)
thread
.
daemon
=
True
thread
.
start
()
def
close
(
self
):
logger
.
debug
(
"Interactive server server close: {}"
.
format
(
self
))
self
.
closed
=
True
# def __del__(self):
# print("GC: Interactive class been gc")
coco/interface.py
View file @
6e153a1a
...
...
@@ -176,7 +176,7 @@ class SSHInterface(paramiko.ServerInterface):
'command'
:
command
})
self
.
event
.
set
()
return
Fals
e
return
Tru
e
def
check_channel_forward_agent_request
(
self
,
channel
):
logger
.
debug
(
"Check channel forward agent request:
%
s"
%
channel
)
...
...
coco/logger.py
View file @
6e153a1a
...
...
@@ -10,7 +10,7 @@ from .config import config as app_config
def
create_logger
():
level
=
app_config
[
'LOG_LEVEL'
]
log_dir
=
app_config
.
get
(
'LOG_DIR'
)
log_dir
=
app_config
[
'LOG_DIR'
]
log_path
=
os
.
path
.
join
(
log_dir
,
'coco.log'
)
main_setting
=
{
'handlers'
:
[
'console'
,
'file'
],
...
...
coco/models.py
View file @
6e153a1a
...
...
@@ -367,6 +367,11 @@ class TelnetServer(BaseServer):
self
.
system_user
=
system_user
super
(
TelnetServer
,
self
)
.
__init__
(
chan
=
sock
)
@property
def
closed
(
self
):
""" self.chan: socket object """
return
getattr
(
self
.
chan
,
'_closed'
,
False
)
class
Server
(
BaseServer
):
"""
...
...
coco/proxy.py
View file @
6e153a1a
...
...
@@ -52,7 +52,7 @@ class ProxyServer:
return
False
return
True
def
manual_s
et_system_user_username_if_need
(
self
):
def
g
et_system_user_username_if_need
(
self
):
if
self
.
system_user
.
login_mode
==
MANUAL_LOGIN
and
\
not
self
.
system_user
.
username
:
username
=
net_input
(
self
.
client
,
prompt
=
'username: '
,
before
=
1
)
...
...
@@ -63,13 +63,15 @@ class ProxyServer:
def
proxy
(
self
):
if
not
self
.
check_protocol
():
return
self
.
manual_s
et_system_user_username_if_need
()
self
.
g
et_system_user_username_if_need
()
self
.
get_system_user_auth_or_manual_set
()
self
.
server
=
self
.
get_server_conn
()
if
self
.
server
is
None
:
return
session
=
Session
.
new_session
(
self
.
client
,
self
.
server
)
try
:
session
.
bridge
()
finally
:
Session
.
remove_session
(
session
.
id
)
self
.
server
.
close
()
...
...
coco/sshd.py
View file @
6e153a1a
...
...
@@ -58,6 +58,7 @@ class SSHServer:
logger
.
error
(
"Start SSH server error: {}"
.
format
(
e
))
def
handle_connection
(
self
,
sock
,
addr
):
logger
.
debug
(
"Handle new connection from: {}"
.
format
(
addr
))
transport
=
paramiko
.
Transport
(
sock
,
gss_kex
=
False
)
try
:
transport
.
load_server_moduli
()
...
...
coco/utils.py
View file @
6e153a1a
...
...
@@ -303,7 +303,7 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
client
.
send
(
wrap_with_line_feed
(
prompt
,
before
=
before
,
after
=
after
))
while
True
:
data
=
client
.
recv
(
1
0
)
data
=
client
.
recv
(
1
)
if
len
(
data
)
==
0
:
break
# Client input backspace
...
...
@@ -331,18 +331,8 @@ def net_input(client, prompt='Opt> ', sensitive=False, before=0, after=0):
client
.
send
(
b
''
)
continue
# handle shell expect
multi_char_with_enter
=
False
if
len
(
data
)
>
1
and
data
[
-
1
]
in
char
.
ENTER_CHAR_ORDER
:
if
sensitive
:
client
.
send
(
len
(
data
)
*
'*'
)
else
:
client
.
send
(
data
)
input_data
.
append
(
data
[:
-
1
])
multi_char_with_enter
=
True
# If user types ENTER we should get user input
if
data
in
char
.
ENTER_CHAR
or
multi_char_with_enter
:
if
data
in
char
.
ENTER_CHAR
:
client
.
send
(
wrap_with_line_feed
(
b
''
,
after
=
2
))
option
=
parser
.
parse_input
(
input_data
)
del
input_data
[:]
...
...
conf_example.py
View file @
6e153a1a
...
...
@@ -17,6 +17,10 @@ class Config:
# Jumpserver项目的url, api请求注册会使用
# CORE_HOST = os.environ.get("CORE_HOST") or 'http://127.0.0.1:8080'
# Bootstrap Token, 预共享秘钥, 用来注册coco使用的service account和terminal
# 请和jumpserver 配置文件中保持一致,注册完成后可以删除
# BOOTSTRAP_TOKEN = "PleaseChangeMe"
# 启动时绑定的ip, 默认 0.0.0.0
# BIND_HOST = '0.0.0.0'
...
...
locale/en/LC_MESSAGES/coco.mo
View file @
6e153a1a
No preview for this file type
locale/en/LC_MESSAGES/coco.po
View file @
6e153a1a
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-1
0-31 11:49
+0800\n"
"POT-Creation-Date: 2018-1
2-18 20:03
+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,11 +16,11 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:1
3
5
#: coco/app.py:1
4
5
msgid "Connect idle more than {} minutes, disconnect"
msgstr ""
#: coco/interactive.py:8
0
#: coco/interactive.py:8
4
#, python-brace-format
msgid ""
"\n"
...
...
@@ -28,131 +28,127 @@ msgid ""
"{end}{R}{R}"
msgstr ""
#: coco/interactive.py:8
2
#: coco/interactive.py:8
6
#, 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:8
3
#: coco/interactive.py:8
7
#, 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:8
4
#: coco/interactive.py:8
8
#, python-brace-format
msgid "{T}3) Enter {green}p{end} to display the host you have permission.{R}"
msgstr ""
#: coco/interactive.py:8
5
#: coco/interactive.py:8
9
#, python-brace-format
msgid ""
"{T}4) Enter {green}g{end} to display the node that you have permission.{R}"
msgstr ""
#: coco/interactive.py:
86
#: coco/interactive.py:
90
#, 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:
87
#: coco/interactive.py:
91
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr ""
#: coco/interactive.py:
88
#: coco/interactive.py:
92
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr ""
#: coco/interactive.py:
89
#: coco/interactive.py:
93
#, python-brace-format
msgid "{T}
0) Enter {green}q{end} exit
.{R}"
msgid "{T}
8) Enter {green}r{end} to refresh your assets and nodes
.{R}"
msgstr ""
#: coco/interactive.py:159
msgid "No"
#: coco/interactive.py:94
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr ""
#: coco/interactive.py:1
66
msgid "
Name
"
#: coco/interactive.py:1
55
msgid "
Terminal does not support login rdp, please use web terminal to access
"
msgstr ""
#: coco/interactive.py:
166
msgid "Assets"
#: coco/interactive.py:
212
msgid "
No
Assets"
msgstr ""
#: coco/interactive.py:
172
msgid "T
otal: {}
"
#: coco/interactive.py:
275
msgid "T
ips: Enter the asset ID and log directly into the asset.
"
msgstr ""
#: coco/interactive.py:
177
msgid "
Node [ ID.Name(Asset) ]
"
#: coco/interactive.py:
276
msgid "
Page up: P/p
"
msgstr ""
#: coco/interactive.py:
179
msgid "
Enter g+NodeID to display the host under the node, such as g1.
"
#: coco/interactive.py:
277
msgid "
Page down: Enter|N/n
"
msgstr ""
#: coco/interactive.py:
186
msgid "
There is no matched node, please re-enter
"
#: coco/interactive.py:
278
msgid "
BACK: b/q
"
msgstr ""
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "ID"
msgstr ""
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "Hostname"
msgstr ""
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "IP"
msgstr ""
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "LoginAs"
msgstr ""
#: coco/interactive.py:
211
#: coco/interactive.py:
313
msgid "Comment"
msgstr ""
#: coco/interactive.py:
221
#: coco/interactive.py:
322
msgid "Page: {}, Count: {}, Total Page: {}, Total Count: {}"
msgstr ""
#: coco/interactive.py:
296
msgid "
Select a login::
"
#: coco/interactive.py:
394
msgid "
No Nodes
"
msgstr ""
#: coco/interactive.py:319
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
#: coco/interactive.py:398
msgid "Node: [ ID.Name(Asset amount) ]"
msgstr ""
#: coco/interactive.py:40
1
msgid "Tips: Enter
the asset ID and log directly into the asset.
"
#: coco/interactive.py:40
0
msgid "Tips: Enter
g+NodeID to display the host under the node, such as g1
"
msgstr ""
#: coco/interactive.py:402
msgid "Page up: P/p"
msgstr ""
#: coco/interactive.py:403
msgid "Page down: Enter|N/n"
#: coco/interactive.py:408
msgid "There is no matched node, please re-enter"
msgstr ""
#: coco/interactive.py:4
04
msgid "
BACK: B/b
"
#: coco/interactive.py:4
38
msgid "
Select a login::
"
msgstr ""
#: coco/interactive.py:4
25
#: coco/interactive.py:4
61
msgid "No system user"
msgstr ""
...
...
locale/zh_CN/LC_MESSAGES/coco.mo
View file @
6e153a1a
No preview for this file type
locale/zh_CN/LC_MESSAGES/coco.po
View file @
6e153a1a
...
...
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-1
0-31 11:49
+0800\n"
"POT-Creation-Date: 2018-1
2-18 20:04
+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,11 +16,11 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: coco/app.py:1
3
5
#: coco/app.py:1
4
5
msgid "Connect idle more than {} minutes, disconnect"
msgstr "空闲时间超过 {} 分钟,断开连接"
#: coco/interactive.py:8
0
#: coco/interactive.py:8
4
#, python-brace-format
msgid ""
"\n"
...
...
@@ -30,7 +30,7 @@ msgstr ""
"\n"
"{T}{T}{title} {user}, 欢迎使用Jumpserver开源跳板机系统 {end}{R}{R}"
#: coco/interactive.py:8
2
#: coco/interactive.py:8
6
#, python-brace-format
msgid ""
"{T}1) Enter {green}ID{end} directly login or enter {green}part IP, Hostname, "
...
...
@@ -39,7 +39,7 @@ msgstr ""
"{T}1) 输入 {green}ID{end} 直接登录 或 输入{green}部分 IP,主机名,备注{end} 进"
"行搜索登录(如果唯一).{R}"
#: coco/interactive.py:8
3
#: coco/interactive.py:8
7
#, python-brace-format
msgid ""
"{T}2) Enter {green}/{end} + {green}IP, Hostname{end} or {green}Comment {end} "
...
...
@@ -48,122 +48,120 @@ msgstr ""
"{T}2) 输入 {green}/{end} + {green}IP, 主机名{end} or {green}备注 {end}搜索. "
"如: /ip{R}"
#: coco/interactive.py:8
4
#: coco/interactive.py:8
8
#, 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:8
5
#: coco/interactive.py:8
9
#, 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:86
#: coco/interactive.py:90
#, 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 "{T}5) 输入 {green}g{end} + {green}节点ID{end} 显示节点下主机. 如: g1{R}"
msgstr ""
"{T}5) 输入 {green}g{end} + {green}节点ID{end} 显示节点下主机. 如: g1{R}"
#: coco/interactive.py:
87
#: coco/interactive.py:
91
#, python-brace-format
msgid "{T}6) Enter {green}s{end} Chinese-english switch.{R}"
msgstr "{T}6) 输入 {green}s{end} 中/英文切换.{R}"
#: coco/interactive.py:
88
#: coco/interactive.py:
92
#, python-brace-format
msgid "{T}7) Enter {green}h{end} help.{R}"
msgstr "{T}7) 输入 {green}h{end} 帮助.{R}"
#: coco/interactive.py:89
#: coco/interactive.py:93
#, 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
#, python-brace-format
msgid "{T}0) Enter {green}q{end} exit.{R}"
msgstr "{T}0) 输入 {green}q{end} 退出.{R}"
#: coco/interactive.py:159
msgid "No"
msgstr "无"
#: coco/interactive.py:166
msgid "Name"
msgstr "名称"
#: coco/interactive.py:155
msgid "Terminal does not support login rdp, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:
166
msgid "Assets"
msgstr "资产"
#: coco/interactive.py:
212
msgid "
No
Assets"
msgstr "
没有
资产"
#: coco/interactive.py:
172
msgid "T
otal: {}
"
msgstr "
总共: {}
"
#: coco/interactive.py:
275
msgid "T
ips: Enter the asset ID and log directly into the asset.
"
msgstr "
提示: 输入资产ID,直接登录资产.
"
#: coco/interactive.py:
177
msgid "
Node: [ ID.Name(Asset amount) ]
"
msgstr "
节点: [ ID.名称(资产数量) ]
"
#: coco/interactive.py:
276
msgid "
Page up: P/p
"
msgstr "
上一页: P/p
"
#: coco/interactive.py:
179
msgid "
Tips: Enter g+NodeID to display the host under the node, such as g1
"
msgstr "
提示: 输入 g+节点ID 显示节点下主机. 如: g1
"
#: coco/interactive.py:
277
msgid "
Page down: Enter|N/n
"
msgstr "
下一页: Enter|N/n
"
#: coco/interactive.py:
186
msgid "
There is no matched node, please re-enter
"
msgstr "
没有匹配分组,请重新输入
"
#: coco/interactive.py:
278
msgid "
BACK: b/q
"
msgstr "
返回: B/b
"
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "ID"
msgstr ""
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "Hostname"
msgstr "主机名"
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "IP"
msgstr ""
#: coco/interactive.py:
197
#: coco/interactive.py:
299
msgid "LoginAs"
msgstr "登录用户"
#: coco/interactive.py:
211
#: coco/interactive.py:
313
msgid "Comment"
msgstr "备注"
#: coco/interactive.py:
221
#: coco/interactive.py:
322
msgid "Page: {}, Count: {}, Total Page: {}, Total Count: {}"
msgstr "页码: {}, 数量: {}, 总页数: {}, 总数量: {}"
#: coco/interactive.py:296
msgid "Select a login:: "
msgstr "选择一个登录:"
#: coco/interactive.py:319
msgid ""
"Terminal does not support login Windows, please use web terminal to access"
msgstr "终端不支持登录windows, 请使用web terminal访问"
#: coco/interactive.py:394
msgid "No Nodes"
msgstr "没有节点"
#: coco/interactive.py:
401
msgid "
Tips: Enter the asset ID and log directly into the asset.
"
msgstr "
提示: 输入资产ID,直接登录资产.
"
#: coco/interactive.py:
398
msgid "
Node: [ ID.Name(Asset amount) ]
"
msgstr "
节点: [ ID.名称(资产数量) ]
"
#: coco/interactive.py:40
2
msgid "
Page up: P/p
"
msgstr "
上一页: P/p
"
#: coco/interactive.py:40
0
msgid "
Tips: Enter g+NodeID to display the host under the node, such as g1
"
msgstr "
提示: 输入 g+节点ID 显示节点下主机. 如: g1
"
#: coco/interactive.py:40
3
msgid "
Page down: Enter|N/n
"
msgstr "
下一页: Enter|N/n
"
#: coco/interactive.py:40
8
msgid "
There is no matched node, please re-enter
"
msgstr "
没有匹配分组,请重新输入
"
#: coco/interactive.py:4
04
msgid "
BACK: B/b
"
msgstr "
返回: B/b
"
#: coco/interactive.py:4
38
msgid "
Select a login::
"
msgstr "
选择一个登录:
"
#: coco/interactive.py:4
25
#: coco/interactive.py:4
61
msgid "No system user"
msgstr "没有系统用户"
#: coco/models.py:247
msgid "Command `{}` is forbidden ........"
msgstr ""
msgstr "
命令 `{}` 是被禁止的 ...
"
#: coco/proxy.py:89
msgid "No permission"
...
...
@@ -177,5 +175,14 @@ msgstr "开始连接到 {}@{} {:.1f}"
msgid "Terminated by administrator"
msgstr "被管理员中断"
#~ msgid "No"
#~ msgstr "无"
#~ msgid "Name"
#~ msgstr "名称"
#~ msgid "Total: {}"
#~ msgstr "总共: {}"
#~ msgid "Total: {} Match: {}"
#~ msgstr "总共: {} 匹配: {}"
requirements/alpine_requirements.txt
0 → 100644
View file @
6e153a1a
krb5-dev sshpass libffi-dev libressl-dev linux-headers
requirements/requirements.txt
View file @
6e153a1a
...
...
@@ -19,7 +19,7 @@ itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
jms-storage==0.0.20
jumpserver-python-sdk==0.0.5
1
jumpserver-python-sdk==0.0.5
3
MarkupSafe==1.0
oss2==2.4.0
paramiko==2.4.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