Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
J
jumpserver
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
jumpserver
Commits
91f64ecc
Unverified
Commit
91f64ecc
authored
Sep 25, 2019
by
老广
Committed by
GitHub
Sep 25, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3283 from jumpserver/dev
Dev
parents
09565375
3c12c339
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
79 additions
and
57 deletions
+79
-57
asset.py
apps/assets/forms/asset.py
+6
-4
node.py
apps/assets/models/node.py
+0
-4
asset.py
apps/assets/serializers/asset.py
+1
-1
utils.py
apps/assets/utils.py
+7
-7
serializers.py
apps/audits/serializers.py
+1
-0
signals_handler.py
apps/audits/signals_handler.py
+5
-1
common.py
apps/common/utils/common.py
+1
-2
settings.py
apps/jumpserver/settings.py
+1
-1
views.py
apps/jumpserver/views.py
+1
-1
django.mo
apps/locale/zh/LC_MESSAGES/django.mo
+0
-0
django.po
apps/locale/zh/LC_MESSAGES/django.po
+27
-28
inventory.py
apps/ops/inventory.py
+2
-2
mixin.py
apps/perms/api/user_permission/mixin.py
+1
-1
utils.py
apps/settings/utils.py
+2
-0
session.py
apps/terminal/api/session.py
+6
-1
models.py
apps/terminal/models.py
+10
-0
session_list.html
apps/terminal/templates/terminal/session_list.html
+2
-2
terminal.py
apps/terminal/views/terminal.py
+1
-1
jms
jms
+5
-1
No files found.
apps/assets/forms/asset.py
View file @
91f64ecc
...
...
@@ -14,6 +14,10 @@ __all__ = [
'AssetCreateForm'
,
'AssetUpdateForm'
,
'AssetBulkUpdateForm'
,
'ProtocolForm'
,
]
HELP_TEXTS_ASSET_HOSTNAME
=
_
(
'Only Numbers、letters、 chinese and characters ( {} ) are allowed'
)
.
format
(
" "
.
join
([
'.'
,
'_'
,
'@'
]))
class
ProtocolForm
(
forms
.
Form
):
name
=
forms
.
ChoiceField
(
...
...
@@ -68,8 +72,7 @@ class AssetCreateForm(OrgModelForm):
'nodes'
:
_
(
"Node"
),
}
help_texts
=
{
'hostname'
:
_
(
'Only Numbers, letters, and characters ( {} ) '
'are allowed'
)
.
format
(
" "
.
join
([
'.'
,
'_'
,
'@'
])),
'hostname'
:
HELP_TEXTS_ASSET_HOSTNAME
,
'admin_user'
:
_
(
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
...
...
@@ -116,8 +119,7 @@ class AssetUpdateForm(OrgModelForm):
'nodes'
:
_
(
"Node"
),
}
help_texts
=
{
'hostname'
:
_
(
'Only Numbers, letters, and characters ( {} ) '
'are allowed'
)
.
format
(
" "
.
join
([
'.'
,
'_'
,
'@'
])),
'hostname'
:
HELP_TEXTS_ASSET_HOSTNAME
,
'admin_user'
:
_
(
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
...
...
apps/assets/models/node.py
View file @
91f64ecc
...
...
@@ -257,10 +257,6 @@ class NodeAssetsMixin:
@lazyproperty
def
assets_amount
(
self
):
"""
获取节点下所有资产数量速度太慢,所以需要重写,使用cache等方案
:return:
"""
amount
=
self
.
tree
()
.
assets_amount
(
self
.
key
)
return
amount
...
...
apps/assets/serializers/asset.py
View file @
91f64ecc
...
...
@@ -94,7 +94,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
@staticmethod
def
validate_hostname
(
hostname
):
pattern
=
r"^[\._@
a-zA-Z0-9
-]+$"
pattern
=
r"^[\._@
\w
-]+$"
res
=
re
.
match
(
pattern
,
hostname
)
if
res
is
None
:
msg
=
_
(
"* The hostname contains characters that are not allowed"
)
...
...
apps/assets/utils.py
View file @
91f64ecc
...
...
@@ -26,10 +26,6 @@ def get_system_user_by_id(id):
class
TreeService
(
Tree
):
tag_sep
=
' / '
cache_key
=
'_NODE_FULL_TREE'
cache_time
=
3600
has_empty_node
=
False
has_ungrouped_node
=
False
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
()
.
__init__
(
*
args
,
**
kwargs
)
...
...
@@ -43,13 +39,17 @@ class TreeService(Tree):
from
orgs.utils
import
tmp_to_root_org
with
tmp_to_root_org
():
all_nodes
=
Node
.
objects
.
all
()
all_nodes
=
list
(
Node
.
objects
.
all
()
.
values
(
"key"
,
"value"
))
all_nodes
.
sort
(
key
=
lambda
x
:
len
(
x
[
"key"
]
.
split
(
":"
)))
tree
=
cls
()
tree
.
create_node
(
tag
=
''
,
identifier
=
''
)
for
node
in
all_nodes
:
key
=
node
[
"key"
]
value
=
node
[
"value"
]
parent_key
=
":"
.
join
(
key
.
split
(
":"
)[:
-
1
])
tree
.
create_node
(
tag
=
node
.
value
,
identifier
=
node
.
key
,
parent
=
node
.
parent_key
,
tag
=
value
,
identifier
=
key
,
parent
=
parent_key
,
)
tree
.
init_assets
()
return
tree
...
...
apps/audits/serializers.py
View file @
91f64ecc
...
...
@@ -36,3 +36,4 @@ class SessionAuditSerializer(serializers.ModelSerializer):
class
Meta
:
model
=
Session
fields
=
'__all__'
apps/audits/signals_handler.py
View file @
91f64ecc
...
...
@@ -9,7 +9,8 @@ from rest_framework.renderers import JSONRenderer
from
jumpserver.utils
import
current_request
from
common.utils
import
get_request_ip
,
get_logger
,
get_syslogger
from
users.models
import
User
from
terminal.models
import
Session
from
terminal.models
import
Session
,
Command
from
terminal.backends.command.serializers
import
SessionCommandSerializer
from
.
import
models
from
.
import
serializers
...
...
@@ -88,6 +89,9 @@ def on_audits_log_create(sender, instance=None, **kwargs):
elif
sender
==
Session
:
category
=
"host_session_log"
serializer
=
serializers
.
SessionAuditSerializer
elif
sender
==
Command
:
category
=
"session_command_log"
serializer
=
SessionCommandSerializer
else
:
return
...
...
apps/common/utils/common.py
View file @
91f64ecc
...
...
@@ -225,4 +225,4 @@ class lazyproperty:
else
:
value
=
self
.
func
(
instance
)
setattr
(
instance
,
self
.
func
.
__name__
,
value
)
return
value
\ No newline at end of file
return
value
apps/jumpserver/settings.py
View file @
91f64ecc
...
...
@@ -286,7 +286,7 @@ LOGGING = {
'handlers'
:
[
'console'
,
'file'
],
'level'
:
"INFO"
,
},
'jms
_
audits'
:
{
'jms
.
audits'
:
{
'handlers'
:
[
'syslog'
],
'level'
:
'INFO'
},
...
...
apps/jumpserver/views.py
View file @
91f64ecc
...
...
@@ -190,7 +190,7 @@ class IndexView(PermissionsMixin, TemplateView):
class
LunaView
(
View
):
def
get
(
self
,
request
):
msg
=
_
(
"<div>Luna is a separately deployed program, you need to deploy Luna,
coc
o, configure nginx for url distribution,</div> "
msg
=
_
(
"<div>Luna is a separately deployed program, you need to deploy Luna,
kok
o, configure nginx for url distribution,</div> "
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>"
)
return
HttpResponse
(
msg
)
...
...
apps/locale/zh/LC_MESSAGES/django.mo
View file @
91f64ecc
No preview for this file type
apps/locale/zh/LC_MESSAGES/django.po
View file @
91f64ecc
...
...
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-2
4 11
:11+0800\n"
"POT-Creation-Date: 2019-09-2
5 15
:11+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
...
...
@@ -112,7 +112,7 @@ msgstr "资产"
#: applications/templates/applications/remote_app_detail.html:53
#: applications/templates/applications/remote_app_list.html:20
#: applications/templates/applications/user_remote_app_list.html:16
#: assets/forms/asset.py:2
0
assets/forms/domain.py:73 assets/forms/user.py:74
#: assets/forms/asset.py:2
4
assets/forms/domain.py:73 assets/forms/user.py:74
#: assets/forms/user.py:94 assets/models/base.py:28 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:20 assets/models/domain.py:20
#: assets/models/group.py:20 assets/models/label.py:18
...
...
@@ -576,14 +576,18 @@ msgstr "更新节点资产硬件信息: {}"
msgid "Test if the assets under the node are connectable: {}"
msgstr "测试节点下资产是否可连接: {}"
#: assets/forms/asset.py:24 assets/models/asset.py:140
#: assets/forms/asset.py:18
msgid "Only Numbers、letters、 chinese and characters ( {} ) are allowed"
msgstr "只允许包含数字、字母、中文和特殊字符( {} )"
#: assets/forms/asset.py:28 assets/models/asset.py:140
#: assets/models/domain.py:50
#: assets/templates/assets/domain_gateway_list.html:69
#: settings/templates/settings/replay_storage_create.html:59
msgid "Port"
msgstr "端口"
#: assets/forms/asset.py:5
5
assets/models/asset.py:145
#: assets/forms/asset.py:5
9
assets/models/asset.py:145
#: assets/models/user.py:110 assets/templates/assets/asset_detail.html:190
#: assets/templates/assets/asset_detail.html:198
#: assets/templates/assets/system_user_assets.html:83
...
...
@@ -594,7 +598,7 @@ msgstr "端口"
msgid "Nodes"
msgstr "节点"
#: assets/forms/asset.py:
58 assets/forms/asset.py:106
#: assets/forms/asset.py:
62 assets/forms/asset.py:109
#: assets/models/asset.py:149 assets/models/cluster.py:19
#: assets/models/user.py:68 assets/templates/assets/asset_detail.html:76
#: templates/_nav.html:44 xpack/plugins/cloud/models.py:161
...
...
@@ -603,7 +607,7 @@ msgstr "节点"
msgid "Admin user"
msgstr "管理用户"
#: assets/forms/asset.py:6
1 assets/forms/asset.py:109 assets/forms/asset.py:150
#: assets/forms/asset.py:6
5 assets/forms/asset.py:112 assets/forms/asset.py:152
#: assets/templates/assets/asset_create.html:48
#: assets/templates/assets/asset_create.html:50
#: assets/templates/assets/asset_list.html:85
...
...
@@ -611,7 +615,7 @@ msgstr "管理用户"
msgid "Label"
msgstr "标签"
#: assets/forms/asset.py:6
4 assets/forms/asset.py:112
#: assets/forms/asset.py:6
8 assets/forms/asset.py:115
#: assets/models/asset.py:144 assets/models/domain.py:26
#: assets/models/domain.py:52 assets/templates/assets/asset_detail.html:80
#: assets/templates/assets/user_asset_list.html:53
...
...
@@ -619,8 +623,8 @@ msgstr "标签"
msgid "Domain"
msgstr "网域"
#: assets/forms/asset.py:
68 assets/forms/asset.py:103 assets/forms/asset.py:116
#: assets/forms/asset.py:15
3 assets/models/node.py:40
3
#: assets/forms/asset.py:
72 assets/forms/asset.py:106 assets/forms/asset.py:119
#: assets/forms/asset.py:15
5 assets/models/node.py:41
3
#: assets/templates/assets/asset_create.html:42
#: perms/forms/asset_permission.py:82 perms/forms/asset_permission.py:89
#: perms/templates/perms/asset_permission_list.html:53
...
...
@@ -635,11 +639,7 @@ msgstr "网域"
msgid "Node"
msgstr "节点"
#: assets/forms/asset.py:71 assets/forms/asset.py:119
msgid "Only Numbers, letters, and characters ( {} ) are allowed"
msgstr "只允许包含数字,字母,特殊字符( {} )"
#: assets/forms/asset.py:74 assets/forms/asset.py:122
#: assets/forms/asset.py:77 assets/forms/asset.py:124
msgid ""
"root or other NOPASSWD sudo privilege user existed in asset,If asset is "
"windows or other set any one, more see admin user left menu"
...
...
@@ -647,16 +647,16 @@ msgstr ""
"root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置一"
"个, 更多信息查看左侧 `管理用户` 菜单"
#: assets/forms/asset.py:
77 assets/forms/asset.py:125
#: assets/forms/asset.py:
80 assets/forms/asset.py:127
msgid "Windows 2016 RDP protocol is different, If is window 2016, set it"
msgstr "Windows 2016的RDP协议与之前不同,如果是请设置"
#: assets/forms/asset.py:
78 assets/forms/asset.py:126
#: assets/forms/asset.py:
81 assets/forms/asset.py:128
msgid ""
"If your have some network not connect with each other, you can set domain"
msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录"
#: assets/forms/asset.py:13
3 assets/forms/asset.py:137
#: assets/forms/asset.py:13
5 assets/forms/asset.py:139
#: assets/forms/domain.py:17 assets/forms/label.py:15
#: perms/templates/perms/asset_permission_asset.html:78
#: xpack/plugins/change_auth_plan/forms.py:55
...
...
@@ -1107,7 +1107,7 @@ msgstr "默认资产组"
msgid "User"
msgstr "用户"
#: assets/models/label.py:19 assets/models/node.py:
39
4
#: assets/models/label.py:19 assets/models/node.py:
40
4
#: assets/templates/assets/label_list.html:15 settings/models.py:30
msgid "Value"
msgstr "值"
...
...
@@ -1116,19 +1116,19 @@ msgstr "值"
msgid "Category"
msgstr "分类"
#: assets/models/node.py:
230
#: assets/models/node.py:
163
msgid "New node"
msgstr "新节点"
#: assets/models/node.py:3
1
8
#: assets/models/node.py:3
2
8
msgid "ungrouped"
msgstr "未分组"
#: assets/models/node.py:3
2
0
#: assets/models/node.py:3
3
0
msgid "empty"
msgstr "空"
#: assets/models/node.py:
39
3
#: assets/models/node.py:
40
3
msgid "Key"
msgstr "键"
...
...
@@ -1182,7 +1182,7 @@ msgstr "Shell"
msgid "Login mode"
msgstr "登录模式"
#: assets/models/user.py:16
8
assets/templates/assets/user_asset_list.html:52
#: assets/models/user.py:16
2
assets/templates/assets/user_asset_list.html:52
#: audits/models.py:20 audits/templates/audits/ftp_log_list.html:52
#: audits/templates/audits/ftp_log_list.html:75
#: perms/forms/asset_permission.py:85 perms/forms/remote_app_permission.py:40
...
...
@@ -2733,13 +2733,12 @@ msgstr "仪表盘"
#: jumpserver/views.py:193
msgid ""
"<div>Luna is a separately deployed program, you need to deploy Luna,
coc
o, "
"<div>Luna is a separately deployed program, you need to deploy Luna,
kok
o, "
"configure nginx for url distribution,</div> </div>If you see this page, "
"prove that you are not accessing the nginx listening port. Good luck.</div>"
msgstr ""
"<div>Luna是单独部署的一个程序,你需要部署luna,coco,配置nginx做url分发, </"
"div><div>如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运</"
"div>"
"<div>Luna是单独部署的一个程序,你需要部署luna,koko, </div><div>如果你看到了"
"这个页面,证明你访问的不是nginx监听的端口,祝你好运</div>"
#: ops/api/celery.py:54
msgid "Waiting task start"
...
...
@@ -3916,7 +3915,7 @@ msgstr "您确定删除吗?"
msgid "Search no entry matched in ou {}"
msgstr "在ou:{}中没有匹配条目"
#: settings/utils.py:12
0
#: settings/utils.py:12
2
msgid "The user source is not LDAP"
msgstr "用户来源不是LDAP"
...
...
apps/ops/inventory.py
View file @
91f64ecc
...
...
@@ -15,7 +15,7 @@ logger = get_logger(__file__)
class
JMSBaseInventory
(
BaseInventory
):
windows_ssh_default_s
sh
=
settings
.
WINDOWS_SSH_DEFAULT_SHELL
windows_ssh_default_s
hell
=
settings
.
WINDOWS_SSH_DEFAULT_SHELL
def
convert_to_ansible
(
self
,
asset
,
run_as_admin
=
False
):
info
=
{
...
...
@@ -35,7 +35,7 @@ class JMSBaseInventory(BaseInventory):
if
asset
.
is_windows
():
info
[
"vars"
]
.
update
({
"ansible_connection"
:
"ssh"
,
"ansible_shell_type"
:
self
.
windows_ssh_default_s
sh
,
"ansible_shell_type"
:
self
.
windows_ssh_default_s
hell
,
})
for
label
in
asset
.
labels
.
all
():
info
[
"vars"
]
.
update
({
...
...
apps/perms/api/user_permission/mixin.py
View file @
91f64ecc
...
...
@@ -36,7 +36,7 @@ class UserNodeTreeMixin:
assets_amount
=
self
.
tree
.
assets_amount
(
node
.
key
)
if
assets_amount
==
0
and
node
.
key
!=
Node
.
empty_key
:
continue
node
.
_
assets_amount
=
assets_amount
node
.
assets_amount
=
assets_amount
data
=
ParserNode
.
parse_node_to_tree_node
(
node
)
_queryset
.
append
(
data
)
return
_queryset
...
...
apps/settings/utils.py
View file @
91f64ecc
...
...
@@ -93,6 +93,8 @@ class LDAPUtil:
user_item
=
self
.
_ldap_entry_to_user_item
(
entry
)
user
=
self
.
get_user_by_username
(
user_item
[
'username'
])
user_item
[
'existing'
]
=
bool
(
user
)
if
user_item
in
user_items
:
continue
user_items
.
append
(
user_item
)
return
user_items
...
...
apps/terminal/api/session.py
View file @
91f64ecc
...
...
@@ -26,7 +26,7 @@ logger = get_logger(__name__)
class
SessionViewSet
(
OrgBulkModelViewSet
):
queryset
=
Session
.
objects
.
all
()
serializer_class
=
serializers
.
SessionSerializer
permission_classes
=
(
IsOrgAdminOrAppUser
|
IsOrgAuditor
,
)
permission_classes
=
(
IsOrgAdminOrAppUser
,
)
filter_fields
=
[
"user"
,
"asset"
,
"system_user"
,
"remote_addr"
,
"protocol"
,
"terminal"
,
"is_finished"
,
...
...
@@ -53,6 +53,11 @@ class SessionViewSet(OrgBulkModelViewSet):
serializer
.
validated_data
[
"system_user"
]
=
_system_user
.
name
return
super
()
.
perform_create
(
serializer
)
def
get_permissions
(
self
):
if
self
.
request
.
method
.
lower
()
in
[
'get'
]:
self
.
permission_classes
=
(
IsOrgAdminOrAppUser
|
IsOrgAuditor
,
)
return
super
()
.
get_permissions
()
class
SessionReplayViewSet
(
viewsets
.
ViewSet
):
serializer_class
=
serializers
.
ReplaySerializer
...
...
apps/terminal/models.py
View file @
91f64ecc
...
...
@@ -4,6 +4,7 @@ import os
import
uuid
from
django.db
import
models
from
django.db.models.signals
import
post_save
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils
import
timezone
from
django.conf
import
settings
...
...
@@ -267,7 +268,16 @@ class Task(models.Model):
db_table
=
"terminal_task"
class
CommandManager
(
models
.
Manager
):
def
bulk_create
(
self
,
objs
,
**
kwargs
):
resp
=
super
()
.
bulk_create
(
objs
,
**
kwargs
)
for
i
in
objs
:
post_save
.
send
(
i
.
__class__
,
instance
=
i
,
created
=
True
)
return
resp
class
Command
(
AbstractSessionCommand
):
objects
=
CommandManager
()
class
Meta
:
db_table
=
"terminal_command"
...
...
apps/terminal/templates/terminal/session_list.html
View file @
91f64ecc
...
...
@@ -41,7 +41,7 @@
</table>
<div
id=
"actions"
class=
"hide"
>
{% if type == "online" %}
{% if type == "online"
and request.user.can_admin_current_org
%}
<div
class=
"input-group"
>
<select
class=
"form-control m-b"
style=
"width: auto"
id=
"slct_bulk_update"
>
<option
value=
"terminate"
>
{% trans 'Terminate selected' %}
</option>
...
...
@@ -140,7 +140,7 @@ function initTable() {
replayBtn
=
replayBtn
.
replace
(
"disabled"
,
""
)
}
var
termBtn
=
'<a class="btn btn-xs btn-danger btn-term" disabled value="sessionID" terminal="terminalID" >{% trans "Terminate" %}</a>'
;
if
(
"{{ request.user.
is_org_admin
}}"
===
"True"
)
{
if
(
"{{ request.user.
can_admin_current_org
}}"
===
"True"
)
{
termBtn
=
termBtn
.
replace
(
"disabled"
,
""
)
.
replace
(
"sessionID"
,
cellData
)
.
replace
(
"terminalID"
,
rowData
.
terminal
)
...
...
apps/terminal/views/terminal.py
View file @
91f64ecc
...
...
@@ -135,4 +135,4 @@ class WebTerminalView(View):
class
WebSFTPView
(
View
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
return
redirect
(
'/
coc
o/elfinder/sftp/?'
+
request
.
GET
.
urlencode
())
return
redirect
(
'/
kok
o/elfinder/sftp/?'
+
request
.
GET
.
urlencode
())
jms
View file @
91f64ecc
...
...
@@ -205,6 +205,10 @@ def parse_service(s):
]
if
s
==
'all'
:
return
all_services
elif
s
==
"web"
:
return
[
'gunicorn'
,
'flower'
]
elif
s
==
"task"
:
return
[
"celery_ansible"
,
"celery_default"
,
"beat"
]
elif
s
==
'gunicorn'
:
return
[
'gunicorn'
,
'flower'
]
elif
s
==
"celery"
:
...
...
@@ -474,7 +478,7 @@ if __name__ == '__main__':
)
parser
.
add_argument
(
"service"
,
type
=
str
,
default
=
"all"
,
nargs
=
"?"
,
choices
=
(
"all"
,
"gunicorn"
,
"celery"
,
"beat"
,
"celery,beat"
,
"flower"
),
choices
=
(
"all"
,
"
web"
,
"task"
,
"
gunicorn"
,
"celery"
,
"beat"
,
"celery,beat"
,
"flower"
),
help
=
"The service to start"
,
)
parser
.
add_argument
(
'-d'
,
'--daemon'
,
nargs
=
"?"
,
const
=
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