Commit 08edda35 authored by ibuler's avatar ibuler

[Update] 完成资产书

parent c8728cac
...@@ -96,4 +96,5 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView): ...@@ -96,4 +96,5 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
def perform_update(self, serializer): def perform_update(self, serializer):
assets = serializer.validated_data.get('assets') assets = serializer.validated_data.get('assets')
instance = self.get_object() instance = self.get_object()
instance.assets.remove(*tuple(assets)) if instance != Node.root():
instance.assets.remove(*tuple(assets))
...@@ -64,7 +64,10 @@ class AssetUpdateForm(forms.ModelForm): ...@@ -64,7 +64,10 @@ class AssetUpdateForm(forms.ModelForm):
'ip': '* required', 'ip': '* required',
'port': '* required', 'port': '* required',
'cluster': '* required', 'cluster': '* required',
'admin_user': _('') 'admin_user': _(
'Admin user is a privilege user exist on this asset,'
'Example: root or other NOPASSWD sudo privilege user'
)
} }
......
...@@ -238,6 +238,13 @@ class SystemUser(AssetUser): ...@@ -238,6 +238,13 @@ class SystemUser(AssetUser):
'auto_push': self.auto_push, 'auto_push': self.auto_push,
} }
@property
def assets(self):
assets = set()
for node in self.nodes.all():
assets.update(set(node.get_all_assets()))
return assets
@property @property
def assets_connective(self): def assets_connective(self):
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {}) _result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {})
......
...@@ -14,11 +14,12 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -14,11 +14,12 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
assets_granted = AssetGrantedSerializer(many=True, read_only=True) assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField() assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField() parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta: class Meta:
model = Node model = Node
fields = [ fields = [
'id', 'key', 'value', 'parent', 'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'assets_granted', 'assets_amount',
] ]
...@@ -26,6 +27,10 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -26,6 +27,10 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
def get_assets_amount(obj): def get_assets_amount(obj):
return len(obj.assets_granted) return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod @staticmethod
def get_parent(obj): def get_parent(obj):
return obj.parent.id return obj.parent.id
......
...@@ -33,8 +33,7 @@ class SystemUserSerializer(serializers.ModelSerializer): ...@@ -33,8 +33,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod
def get_assets_amount(obj): def get_assets_amount(obj):
amount = 0 return len(obj.assets)
return amount
class AssetSystemUserSerializer(serializers.ModelSerializer): class AssetSystemUserSerializer(serializers.ModelSerializer):
......
...@@ -24,9 +24,15 @@ def test_asset_conn_on_created(asset): ...@@ -24,9 +24,15 @@ def test_asset_conn_on_created(asset):
test_asset_connectability_util.delay(asset) test_asset_connectability_util.delay(asset)
def set_asset_root_node(asset):
logger.debug("Set asset default node: {}".format(Node.root()))
asset.nodes.add(Node.root())
@receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_created(sender, instance=None, created=False, **kwargs): def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
if instance and created: set_asset_root_node(instance)
if created:
logger.info("Asset `{}` create signal received".format(instance)) logger.info("Asset `{}` create signal received".format(instance))
update_asset_hardware_info_on_created(instance) update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance) test_asset_conn_on_created(instance)
......
...@@ -166,6 +166,8 @@ def test_admin_user_connectability_util(admin_user, task_name): ...@@ -166,6 +166,8 @@ def test_admin_user_connectability_util(admin_user, task_name):
assets = admin_user.get_related_assets() assets = admin_user.get_related_assets()
hosts = [asset.hostname for asset in assets] hosts = [asset.hostname for asset in assets]
if not hosts:
return
tasks = const.TEST_ADMIN_USER_CONN_TASKS tasks = const.TEST_ADMIN_USER_CONN_TASKS
task, created = update_or_create_ansible_task( task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all', task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
...@@ -184,19 +186,10 @@ def test_admin_user_connectability_period(): ...@@ -184,19 +186,10 @@ def test_admin_user_connectability_period():
""" """
A period task that update the ansible task period A period task that update the ansible task period
""" """
from ops.utils import update_or_create_ansible_task
admin_users = AdminUser.objects.all() admin_users = AdminUser.objects.all()
for admin_user in admin_users: for admin_user in admin_users:
task_name = _("Test admin user connectability period: {}").format(admin_user) task_name = _("Test admin user connectability period: {}".format(admin_user.name))
assets = admin_user.get_related_assets() test_admin_user_connectability_util(admin_user, task_name)
hosts = [asset.hostname for asset in assets]
tasks = const.TEST_ADMIN_USER_CONN_TASKS
update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
interval=3600, is_periodic=True,
callback=set_admin_user_connectability_info.name,
)
@shared_task @shared_task
...@@ -262,23 +255,21 @@ def test_system_user_connectability_util(system_user, task_name): ...@@ -262,23 +255,21 @@ def test_system_user_connectability_util(system_user, task_name):
:param task_name: :param task_name:
:return: :return:
""" """
# todo from ops.utils import update_or_create_ansible_task
# from ops.utils import update_or_create_ansible_task assets = system_user.assets
# assets = system_user.get_clusters_assets() hosts = [asset.hostname for asset in assets]
# hosts = [asset.hostname for asset in assets] tasks = const.TEST_SYSTEM_USER_CONN_TASKS
# tasks = const.TEST_SYSTEM_USER_CONN_TASKS if not hosts:
# if not hosts: logger.info("No hosts, passed")
# logger.info("No hosts, passed") return {}
# return {} task, created = update_or_create_ansible_task(
# task, created = update_or_create_ansible_task( task_name, hosts=hosts, tasks=tasks, pattern='all',
# task_name, hosts=hosts, tasks=tasks, pattern='all', options=const.TASK_OPTIONS,
# options=const.TASK_OPTIONS, run_as=system_user.name, created_by="System",
# run_as=system_user.name, created_by="System", )
# ) result = task.run()
# result = task.run() set_system_user_connectablity_info(result, system_user=system_user.name)
# set_system_user_connectablity_info(result, system_user=system_user.name) return result
# return result
return {}
@shared_task @shared_task
...@@ -292,23 +283,10 @@ def test_system_user_connectability_manual(system_user): ...@@ -292,23 +283,10 @@ def test_system_user_connectability_manual(system_user):
@after_app_ready_start @after_app_ready_start
@after_app_shutdown_clean @after_app_shutdown_clean
def test_system_user_connectability_period(): def test_system_user_connectability_period():
# Todo system_users = SystemUser.objects.all()
pass for system_user in system_users:
# from ops.utils import update_or_create_ansible_task task_name = _("test system user connectability period: {}".format(system_user))
# system_users = SystemUser.objects.all() test_system_user_connectability_util(system_user, task_name)
# for system_user in system_users:
# task_name = _("Test system user connectability period: {}").format(
# system_user.name
# )
# assets = system_user.get_clusters_assets()
# hosts = [asset.hostname for asset in assets]
# tasks = const.TEST_SYSTEM_USER_CONN_TASKS
# update_or_create_ansible_task(
# task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
# options=const.TASK_OPTIONS, run_as_admin=False, run_as=system_user.name,
# created_by='System', interval=3600, is_periodic=True,
# callback=set_admin_user_connectability_info.name,
# )
#### Push system user tasks #### #### Push system user tasks ####
...@@ -416,10 +394,10 @@ def push_node_system_users_to_asset(node, assets): ...@@ -416,10 +394,10 @@ def push_node_system_users_to_asset(node, assets):
push_system_user_util.delay(system_users, assets, task_name) push_system_user_util.delay(system_users, assets, task_name)
@shared_task # @shared_task
@register_as_period_task(interval=3600) # @register_as_period_task(interval=3600)
@after_app_ready_start # @after_app_ready_start
@after_app_shutdown_clean # # @after_app_shutdown_clean
def push_system_user_period(): # def push_system_user_period():
for system_user in SystemUser.objects.all(): # for system_user in SystemUser.objects.all():
push_system_user_related_nodes(system_user) # push_system_user_related_nodes(system_user)
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
管理用户是 服务器上已存在的特权用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。可以设置主机级别管理用户,也设置集群级别管理用户,这样资产可以不用再单独设置 管理用户是 服务器上已存在的特权用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
</div> </div>
{% endblock %} {% endblock %}
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
{% block custom_head_css_js %} {% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet"> <link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<style type="text/css"> <style type="text/css">
div#rMenu { div#rMenu {
position:absolute; position:absolute;
......
...@@ -289,5 +289,29 @@ $(document).ready(function () { ...@@ -289,5 +289,29 @@ $(document).ready(function () {
var redirect_url = "{% url 'assets:system-user-list' %}"; var redirect_url = "{% url 'assets:system-user-list' %}";
objectDelete($this, name, the_url, redirect_url); objectDelete($this, name, the_url, redirect_url);
}) })
.on('click', '.btn-push', function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}"
});
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, seen left assets status" %}"
});
})
</script> </script>
{% endblock %} {% endblock %}
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
系统用户是 用户登录资产(服务器)时使用的用户,如 web, sa, dba等具有特殊功能的用户。系统用户创建时,如果选择了自动推送 系统用户是 用户登录资产(服务器)时使用的用户,如 web, sa, dba等具有特殊功能的用户。系统用户创建时,如果选择了自动推送
Jumpserver会使用ansible自动推送到系统用户所在集群的资产中,如果资产(交换机)不支持ansible, 请手动填写账号密码。 Jumpserver会使用ansible自动推送系统用户到资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
</div> </div>
{% endblock %} {% endblock %}
......
...@@ -213,22 +213,19 @@ class AssetExportView(View): ...@@ -213,22 +213,19 @@ class AssetExportView(View):
] ]
] ]
filename = 'assets-{}.csv'.format( filename = 'assets-{}.csv'.format(
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')) timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
)
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="%s"' % filename response['Content-Disposition'] = 'attachment; filename="%s"' % filename
response.write(codecs.BOM_UTF8) response.write(codecs.BOM_UTF8)
assets = Asset.objects.filter(id__in=assets_id) assets = Asset.objects.filter(id__in=assets_id)
writer = csv.writer(response, dialect='excel', writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
quoting=csv.QUOTE_MINIMAL)
header = [field.verbose_name for field in fields] header = [field.verbose_name for field in fields]
header.append(_('Asset groups'))
writer.writerow(header) writer.writerow(header)
for asset in assets: for asset in assets:
groups = ','.join([group.name for group in asset.groups.all()])
data = [getattr(asset, field.name) for field in fields] data = [getattr(asset, field.name) for field in fields]
data.append(groups)
writer.writerow(data) writer.writerow(data)
return response return response
...@@ -262,7 +259,6 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): ...@@ -262,7 +259,6 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
] ]
header_ = csv_data[0] header_ = csv_data[0]
mapping_reverse = {field.verbose_name: field.name for field in fields} mapping_reverse = {field.verbose_name: field.name for field in fields}
mapping_reverse[_('Asset groups')] = 'groups'
attr = [mapping_reverse.get(n, None) for n in header_] attr = [mapping_reverse.get(n, None) for n in header_]
if None in attr: if None in attr:
data = {'valid': False, data = {'valid': False,
...@@ -279,20 +275,15 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): ...@@ -279,20 +275,15 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
asset_dict = dict(zip(attr, row)) asset_dict = dict(zip(attr, row))
id_ = asset_dict.pop('id', 0) id_ = asset_dict.pop('id', 0)
for k, v in asset_dict.items(): for k, v in asset_dict.items():
if k == 'cluster': if k == 'is_active':
v = get_object_or_none(Cluster, name=v) v = True if v in ['TRUE', 1, 'true'] else False
elif k == 'is_active':
v = bool(v)
elif k == 'admin_user': elif k == 'admin_user':
v = get_object_or_none(AdminUser, name=v) v = get_object_or_none(AdminUser, name=v)
elif k in ['port', 'cabinet_pos', 'cpu_count', 'cpu_cores']: elif k in ['port', 'cpu_count', 'cpu_cores']:
try: try:
v = int(v) v = int(v)
except ValueError: except ValueError:
v = 0 v = 0
elif k == 'groups':
groups_name = v.split(',')
v = AssetGroup.objects.filter(name__in=groups_name)
else: else:
continue continue
asset_dict[k] = v asset_dict[k] = v
...@@ -300,20 +291,15 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): ...@@ -300,20 +291,15 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
asset = get_object_or_none(Asset, id=id_) if is_uuid(id_) else None asset = get_object_or_none(Asset, id=id_) if is_uuid(id_) else None
if not asset: if not asset:
try: try:
groups = asset_dict.pop('groups')
if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))): if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))):
raise Exception(_('already exists')) raise Exception(_('already exists'))
asset = Asset.objects.create(**asset_dict) asset = Asset.objects.create(**asset_dict)
asset.groups.set(groups)
created.append(asset_dict['hostname']) created.append(asset_dict['hostname'])
assets.append(asset) assets.append(asset)
except Exception as e: except Exception as e:
failed.append('%s: %s' % (asset_dict['hostname'], str(e))) failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
else: else:
for k, v in asset_dict.items(): for k, v in asset_dict.items():
if k == 'groups':
asset.groups.set(v)
continue
if v: if v:
setattr(asset, k, v) setattr(asset, k, v)
try: try:
......
This diff is collapsed.
...@@ -112,13 +112,13 @@ function initTable() { ...@@ -112,13 +112,13 @@ function initTable() {
} }
}}, }},
{targets: 6, createdCell: function (td, cellData, rowData) { {targets: 6, createdCell: function (td, cellData, rowData) {
var name = rowData.user_group.name + "=>" + rowData.system_user.name + "=>" + rowData.node.name;
var update_btn = '<a href="{% url "perms:asset-permission-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData); var update_btn = '<a href="{% url "perms:asset-permission-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-del" data-uid="{{ DEFAULT_PK }}" data-name="99991938">{% trans "Delete" %}</a>' var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-del" data-uid="{{ DEFAULT_PK }}" data-name="99991938">{% trans "Delete" %}</a>'
.replace('{{ DEFAULT_PK }}', cellData) .replace('{{ DEFAULT_PK }}', cellData)
.replace('99991938', rowData.name); .replace('99991938', name);
$(td).html(update_btn + del_btn); $(td).html(update_btn + del_btn);
}} }}
], ],
ajax_url: '{% url "api-perms:asset-permission-list" %}', ajax_url: '{% url "api-perms:asset-permission-list" %}',
columns: [ columns: [
...@@ -207,8 +207,8 @@ $(document).ready(function(){ ...@@ -207,8 +207,8 @@ $(document).ready(function(){
}) })
.on('click', '.btn-del', function () { .on('click', '.btn-del', function () {
var $this = $(this); var $this = $(this);
var name = $this.data('name');
var uid = $this.data('uid'); var uid = $this.data('uid');
var name = $this.data('name');
var the_url = '{% url "api-perms:asset-permission-detail" pk=DEFAULT_PK %}' var the_url = '{% url "api-perms:asset-permission-detail" pk=DEFAULT_PK %}'
.replace('{{ DEFAULT_PK }}', uid); .replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url); objectDelete($this, name, the_url);
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<i class="fa fa-group" style="font-size: 13px"></i> <span class="nav-label">{% trans 'Users' %}</span><span class="fa arrow"></span> <i class="fa fa-group" style="font-size: 13px"></i> <span class="nav-label">{% trans 'Users' %}</span><span class="fa arrow"></span>
</a> </a>
<ul class="nav nav-second-level active"> <ul class="nav nav-second-level active">
<li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User' %}</a></li> <li id="user"><a href="{% url 'users:user-list' %}">{% trans 'User list' %}</a></li>
<li id="user-group"><a href="{% url 'users:user-group-list' %}">{% trans 'User group' %}</a></li> <li id="user-group"><a href="{% url 'users:user-group-list' %}">{% trans 'User group' %}</a></li>
<li id="login-log"><a href="{% url 'users:login-log-list' %}">{% trans 'Login logs' %}</a></li> <li id="login-log"><a href="{% url 'users:login-log-list' %}">{% trans 'Login logs' %}</a></li>
</ul> </ul>
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<i class="fa fa-inbox"></i> <span class="nav-label">{% trans 'Assets' %}</span><span class="fa arrow"></span> <i class="fa fa-inbox"></i> <span class="nav-label">{% trans 'Assets' %}</span><span class="fa arrow"></span>
</a> </a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="asset"><a href="{% url 'assets:asset-list' %}">{% trans 'Asset' %}</a></li> <li id="asset"><a href="{% url 'assets:asset-list' %}">{% trans 'Asset list' %}</a></li>
<li id="admin-user"><a href="{% url 'assets:admin-user-list' %}">{% trans 'Admin user' %}</a></li> <li id="admin-user"><a href="{% url 'assets:admin-user-list' %}">{% trans 'Admin user' %}</a></li>
<li id="system-user"><a href="{% url 'assets:system-user-list' %}">{% trans 'System user' %}</a></li> <li id="system-user"><a href="{% url 'assets:system-user-list' %}">{% trans 'System user' %}</a></li>
<li id="label"><a href="{% url 'assets:label-list' %}">{% trans 'Labels' %}</a></li> <li id="label"><a href="{% url 'assets:label-list' %}">{% trans 'Labels' %}</a></li>
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
<i class="fa fa-coffee"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span> <i class="fa fa-coffee"></i> <span class="nav-label">{% trans 'Job Center' %}</span><span class="fa arrow"></span>
</a> </a>
<ul class="nav nav-second-level"> <ul class="nav nav-second-level">
<li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task' %}</a></li> <li id="task"><a href="{% url 'ops:task-list' %}">{% trans 'Task list' %}</a></li>
</ul> </ul>
</li> </li>
{#<li id="">#} {#<li id="">#}
......
...@@ -110,10 +110,6 @@ ...@@ -110,10 +110,6 @@
</table> </table>
</td> </td>
</tr> </tr>
<tr>
<td class="text-navy">{% trans 'Perm assets' %}</td>
<td>{{ assets | length }}</td>
</tr>
<tr> <tr>
<td class="text-navy">{% trans 'Comment' %}:</td> <td class="text-navy">{% trans 'Comment' %}:</td>
<td><b>{{ user.comment }}</b></td> <td><b>{{ user.comment }}</b></td>
......
...@@ -308,12 +308,9 @@ class UserProfileView(LoginRequiredMixin, TemplateView): ...@@ -308,12 +308,9 @@ class UserProfileView(LoginRequiredMixin, TemplateView):
template_name = 'users/user_profile.html' template_name = 'users/user_profile.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
from perms.utils import get_user_granted_assets
assets = get_user_granted_assets(self.request.user)
context = { context = {
'app': _('Users'), 'app': _('Users'),
'action': _('Profile'), 'action': _('Profile'),
'assets': assets,
} }
kwargs.update(context) kwargs.update(context)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment