diff --git a/apps/assets/__init__.py b/apps/assets/__init__.py index e69de29bb..a3128cc9a 100644 --- a/apps/assets/__init__.py +++ b/apps/assets/__init__.py @@ -0,0 +1 @@ +from . import signals diff --git a/apps/assets/api.py b/apps/assets/api.py index d2ba7ca61..6df0c184e 100644 --- a/apps/assets/api.py +++ b/apps/assets/api.py @@ -181,10 +181,11 @@ class AssetRefreshHardwareView(generics.RetrieveAPIView): asset_id = kwargs.get('pk') asset = get_object_or_404(Asset, pk=asset_id) summary = update_assets_hardware_info([asset]) - if len(summary['failed']) == 0: - return super(AssetRefreshHardwareView, self).retrieve(request, *args, **kwargs) + print(summary) + if summary.get('dark'): + return Response(summary['dark'].values(), status=501) else: - return Response('', status=502) + return Response({"msg": "ok"}) class AssetAdminUserTestView(AssetRefreshHardwareView): diff --git a/apps/assets/apps.py b/apps/assets/apps.py index 89c61e1db..41eead508 100644 --- a/apps/assets/apps.py +++ b/apps/assets/apps.py @@ -5,3 +5,8 @@ from django.apps import AppConfig class AssetsConfig(AppConfig): name = 'assets' + + def ready(self): + from .signals import on_app_ready + on_app_ready.send(self.__class__) + super().ready() diff --git a/apps/assets/const.py b/apps/assets/const.py new file mode 100644 index 000000000..36a539c20 --- /dev/null +++ b/apps/assets/const.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# + +ADMIN_USER_CONN_CACHE_KEY_PREFIX = "ADMIN_USER_CONN_" +SYSTEM_USER_CONN_CACHE_KEY_PREFIX = 'SYSTEM_USER_CONN_' diff --git a/apps/assets/forms.py b/apps/assets/forms.py index 5b7d30d5a..a067a4a3c 100644 --- a/apps/assets/forms.py +++ b/apps/assets/forms.py @@ -10,52 +10,26 @@ logger = get_logger(__file__) class AssetCreateForm(forms.ModelForm): - # Form field name can not start with `_`, so redefine it, - password = forms.CharField( - widget=forms.PasswordInput, max_length=100, - strip=True, required=False, - help_text=_('If also set private key, use that first'), - ) - # Need use upload private key file except paste private key content - private_key_file = forms.FileField(required=False) - - def save(self, commit=True): - # Because we define custom field, so we need rewrite :method: `save` - obj = super().save(commit=commit) - password = self.cleaned_data['password'] - private_key = self.cleaned_data['private_key_file'] - - if password: - obj.password = password - if private_key: - obj.private_key = private_key - obj.save() - return obj - - def clean_private_key_file(self): - private_key_file = self.cleaned_data['private_key_file'] - if private_key_file: - private_key = private_key_file.read() - if not validate_ssh_private_key(private_key): - raise forms.ValidationError(_('Invalid private key')) - return private_key - return private_key_file class Meta: model = Asset fields = [ 'hostname', 'ip', 'public_ip', 'port', 'type', 'comment', - 'cluster', 'groups', 'status', 'env', 'is_active', 'username', + 'cluster', 'groups', 'status', 'env', 'is_active', + 'admin_user' ] widgets = { - 'groups': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset groups')}), + 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), + 'cluster': forms.Select(attrs={'class': 'select2', 'data-placeholder': _('Select cluster')}), + 'admin_user': forms.Select(attrs={'class': 'select2', 'data-placeholder': _('Select admin user')}), } help_texts = { 'hostname': '* required', 'ip': '* required', + 'port': '* required', + 'cluster': '* required', + 'admin_user': _('Host level admin user, If not set using cluster admin user default') } @@ -65,16 +39,18 @@ class AssetUpdateForm(forms.ModelForm): fields = [ 'hostname', 'ip', 'port', 'groups', "cluster", 'is_active', 'type', 'env', 'status', 'public_ip', 'remote_card_ip', 'cabinet_no', - 'cabinet_pos', 'number', 'comment' + 'cabinet_pos', 'number', 'comment', 'admin_user', ] widgets = { - 'groups': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Select asset groups')}), + 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), + 'admin_user': forms.Select(attrs={'class': 'select2', 'data-placeholder': _("Default using cluster admin user")}) } help_texts = { 'hostname': '* required', 'ip': '* required', + 'port': '* required', + 'cluster': '* required', + 'admin_user': _('Host level admin user, If not set using cluster admin user default') } diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 430183d16..c2622511b 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -3,17 +3,13 @@ # import uuid -import os import logging -from hashlib import md5 from django.db import models -from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.core.cache import cache -from common.utils import signer, ssh_key_string_to_obj -from .utils import private_key_validator +from ..const import ADMIN_USER_CONN_CACHE_KEY_PREFIX from .cluster import Cluster from .group import AssetGroup from .user import AdminUser, SystemUser @@ -59,9 +55,7 @@ class Asset(models.Model): status = models.CharField(choices=STATUS_CHOICES, max_length=12, null=True, blank=True, default='In use', verbose_name=_('Asset status')) # Auth - username = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('Username')) - _password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - _private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ]) + admin_user = models.ForeignKey('assets.AdminUser', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) # Some information public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP')) @@ -105,39 +99,22 @@ class Asset(models.Model): return False, warning @property - def password(self): - if self._password: - return signer.unsign(self._password) + def hardware_info(self): + if self.cpu_count: + return '{} Core {} {}'.format( + self.cpu_count * self.cpu_cores, + self.memory, self.disk_total + ) else: return '' - @password.setter - def password(self, password_raw): - self._password = signer.sign(password_raw) - @property - def private_key(self): - if self._private_key: - key_str = signer.unsign(self._private_key) - return ssh_key_string_to_obj(key_str) + def is_connective(self): + val = cache.get(ADMIN_USER_CONN_CACHE_KEY_PREFIX + self.hostname) + if val == 1: + return True else: - return None - - @private_key.setter - def private_key(self, private_key_raw): - self._private_key = signer.sign(private_key_raw) - - @property - def private_key_file(self): - if not self.private_key: - return None - project_dir = settings.PROJECT_DIR - tmp_dir = os.path.join(project_dir, 'tmp') - key_name = md5(self._private_key.encode()).hexdigest() - key_path = os.path.join(tmp_dir, key_name) - if not os.path.exists(key_path): - self.private_key.write_private_key_file(key_path) - return key_path + return False def to_json(self): return { @@ -148,25 +125,28 @@ class Asset(models.Model): 'groups': [group.name for group in self.groups.all()], } - def is_connective(self): - return cache.get(self.hostname) - def _to_secret_json(self): """ - Ansible use it create inventory + Ansible use it create inventory, First using asset user, + otherwise using cluster admin user Todo: May be move to ops implements it """ data = self.to_json() - if self.cluster and self.cluster.admin_user: + admin_user = None + if self.admin_user: + admin_user = self.admin_user + elif self.cluster and self.cluster.admin_user: + admin_user = self.cluster.admin_user + if admin_user: data.update({ - 'username': self.cluster.admin_user.username, - 'password': self.cluster.admin_user.password, - 'private_key': self.cluster.admin_user.private_key_file, + 'username': admin_user.username, + 'password': admin_user.password, + 'private_key': admin_user.private_key_file, 'become': { - 'method': self.cluster.admin_user.become_method, - 'user': self.cluster.admin_user.become_user, - 'pass': self.cluster.admin_user.become_pass, + 'method': admin_user.become_method, + 'user': admin_user.become_user, + 'pass': admin_user.become_pass, } }) return data diff --git a/apps/assets/models/cluster.py b/apps/assets/models/cluster.py index dd30516a4..be90a1be6 100644 --- a/apps/assets/models/cluster.py +++ b/apps/assets/models/cluster.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- # -from __future__ import unicode_literals - import logging import uuid @@ -18,7 +16,7 @@ logger = logging.getLogger(__name__) class Cluster(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=32, verbose_name=_('Name')) - admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.CASCADE, verbose_name=_("Admin user")) + admin_user = models.ForeignKey('assets.AdminUser', null=True, on_delete=models.SET_NULL, verbose_name=_("Admin user")) bandwidth = models.CharField(max_length=32, blank=True, verbose_name=_('Bandwidth')) contact = models.CharField(max_length=128, blank=True, verbose_name=_('Contact')) phone = models.CharField(max_length=32, blank=True, verbose_name=_('Phone')) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index fb8a98ede..e7df0f151 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -96,12 +96,16 @@ class AdminUser(models.Model): def become_pass(self, password): self._become_pass = signer.sign(password) + def get_related_assets(self): + assets = [] + for cluster in self.cluster_set.all(): + assets.extend(cluster.assets.all()) + assets.extend(self.asset_set.all()) + return list(set(assets)) + @property def assets_amount(self): - amount = 0 - for cluster in self.cluster_set.all(): - amount += cluster.assets.all().count() - return amount + return len(self.get_related_assets()) class Meta: ordering = ['name'] @@ -209,9 +213,14 @@ class SystemUser(models.Model): 'private_key_file': self.private_key_file, } + def get_clusters_assets(self): + from .asset import Asset + clusters = self.cluster.all() + return Asset.objects.filter(cluster__in=clusters) + @property def assets_amount(self): - return self.assets.count() + return len(self.get_clusters_assets()) def to_json(self): return { diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py index cfd6d1700..a22de9ef9 100644 --- a/apps/assets/serializers.py +++ b/apps/assets/serializers.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from django.core.cache import cache from rest_framework import viewsets, serializers, generics -from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin +from rest_framework_bulk.serializers import BulkListSerializer +from common.mixins import BulkSerializerMixin from .models import AssetGroup, Asset, Cluster, AdminUser, SystemUser from .tasks import SYSTEM_USER_CONN_CACHE_KEY_PREFIX, ADMIN_USER_CONN_CACHE_KEY_PREFIX @@ -140,36 +141,18 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): - # system_users = SystemUserSerializer(many=True, read_only=True) - # admin_user = AdminUserSerializer(many=False, read_only=True) - hardware = serializers.SerializerMethodField() - is_online = serializers.SerializerMethodField() - class Meta(object): model = Asset list_serializer_class = BulkListSerializer fields = '__all__' - - @staticmethod - def get_hardware(obj): - if obj.cpu_count: - return '{} Core {} {}'.format(obj.cpu_count*obj.cpu_cores, obj.memory, obj.disk_total) - else: - return '' - - @staticmethod - def get_is_online(obj): - hostname = obj.hostname - if cache.get(hostname) == '1': - return True - elif cache.get(hostname) == '0': - return False - else: - return 'Unknown' + validators = [] # If not set to [], partial bulk update will be error def get_field_names(self, declared_fields, info): - fields = super(AssetSerializer, self).get_field_names(declared_fields, info) - fields.extend(['get_type_display', 'get_env_display']) + fields = super().get_field_names(declared_fields, info) + fields.extend([ + 'get_type_display', 'get_env_display', + 'hardware_info', 'is_connective', + ]) return fields diff --git a/apps/assets/signals.py b/apps/assets/signals.py new file mode 100644 index 000000000..d8223eaba --- /dev/null +++ b/apps/assets/signals.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# + +from django.dispatch import Signal, receiver + +from common.utils import get_logger + +logger = get_logger(__file__) +on_asset_created = Signal(providing_args=['asset']) +on_app_ready = Signal() + + +@receiver(on_asset_created) +def update_asset_info(sender, asset=None, **kwargs): + from .tasks import update_assets_hardware_info + logger.debug("Receive asset create signal, update asset hardware info") + update_assets_hardware_info.delay([asset]) + + +@receiver(on_asset_created) +def test_admin_user_connective(sender, asset=None, **kwargs): + from .tasks import test_admin_user_connectability_manual + logger.debug("Receive asset create signal, test admin user connectability") + test_admin_user_connectability_manual.delay(asset) + + +@receiver(on_app_ready) +def test_admin_user_on_app_ready(sender, **kwargs): + from .tasks import test_admin_user_connectability_period + logger.debug("Receive app ready signal, test admin connectability") + test_admin_user_connectability_period.delay() diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index 5ac899abd..46d85db5d 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -4,16 +4,14 @@ import json from celery import shared_task from django.core.cache import cache -from assets.models import SystemUser, AdminUser from common.utils import get_object_or_none, capacity_convert, sum_capacity, encrypt_password, get_logger -from .models import Asset +from .models import SystemUser, AdminUser, Asset +from .const import ADMIN_USER_CONN_CACHE_KEY_PREFIX, SYSTEM_USER_CONN_CACHE_KEY_PREFIX FORKS = 10 TIMEOUT = 60 logger = get_logger(__file__) -ADMIN_USER_CONN_CACHE_KEY_PREFIX = "ADMIN_USER_CONN_" -SYSTEM_USER_CONN_CACHE_KEY_PREFIX = 'SYSTEM_USER_CONN_' @shared_task @@ -75,6 +73,12 @@ def update_assets_hardware_info(assets): if k.startswith('___'): setattr(asset, k.strip('_'), v) asset.save() + + for hostname, task in summary['dark'].items(): + logger.warn("Update {} hardware info error: {}".format( + hostname, task[name], + )) + return summary @@ -96,8 +100,7 @@ def test_admin_user_connectability(admin_user): :return: """ from ops.utils import run_adhoc - assets = admin_user.assets.all() - # assets = Asset.objects.filter(type__in=['Server', 'VM']) + assets = admin_user.get_related_assets() hosts = [asset.hostname for asset in assets] tasks = [ { @@ -126,6 +129,7 @@ def test_admin_user_connectability_period(): cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + i, 0, 60*60*60) +@shared_task def test_admin_user_connectability_manual(asset): from ops.utils import run_adhoc # assets = Asset.objects.filter(type__in=['Server', 'VM']) @@ -140,8 +144,10 @@ def test_admin_user_connectability_manual(asset): ] result = run_adhoc(hosts, tasks=tasks, pattern="all", run_as_admin=True) if result.results_summary['dark']: + cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + asset.hostname, 0, 60*60*60) return False else: + cache.set(ADMIN_USER_CONN_CACHE_KEY_PREFIX + asset.hostname, 1, 60*60* 60) return True @@ -153,7 +159,7 @@ def test_system_user_connectability(system_user): :return: """ from ops.utils import run_adhoc - assets = system_user.assets.all() + assets = system_user.get_clusters_assets() hosts = [asset.hostname for asset in assets] tasks = [ { @@ -171,7 +177,7 @@ def test_system_user_connectability(system_user): def test_system_user_connectability_period(): for system_user in SystemUser.objects.all(): summary = test_system_user_connectability(system_user) - cache.set(SYSTEM_USER_CONN_CACHE_KEY_PREFIX + system_user.name , summary, 60*60*60) + cache.set(SYSTEM_USER_CONN_CACHE_KEY_PREFIX + system_user.name, summary, 60*60*60) def get_push_system_user_tasks(system_user): @@ -207,19 +213,12 @@ def get_push_system_user_tasks(system_user): ) } } - ] return tasks -PUSH_SYSTEM_USER_PERIOD_TASK_NAME = 'PUSH SYSTEM USER {} PERIOD...' -PUSH_SYSTEM_USER_TASK_NAME = 'PUSH SYSTEM USER {} ASSETS' - - -def get_push_system_user_task(system_user): - from ops.utils import get_task_by_name - task = get_task_by_name(PUSH_SYSTEM_USER_PERIOD_TASK_NAME.format(system_user.name)) - return task +PUSH_SYSTEM_USER_PERIOD_TASK_NAME = 'PUSH SYSTEM USER [{}] PERIOD...' +PUSH_SYSTEM_USER_TASK_NAME = 'PUSH SYSTEM USER [{}] ASSETS' def push_system_user(system_user, assets, name): @@ -246,7 +245,7 @@ def push_system_user(system_user, assets, name): def push_system_user_period(): logger.debug("Push system user period") for s in SystemUser.objects.filter(auto_push=True): - assets = s.assets.all() + assets = s.get_clusters_assets() name = PUSH_SYSTEM_USER_PERIOD_TASK_NAME.format(s.name) push_system_user(s, assets, name) diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index 37874ac3c..0db237b46 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -5,7 +5,7 @@ {% block help_message %}
- 管理用户是 服务器上已存在的特权用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等 + 管理用户是 服务器上已存在的特权用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。可以设置主机级别管理用户,也设置集群级别管理用户,这样资产可以不用再单独设置
{% endblock %} diff --git a/apps/assets/templates/assets/asset_create.html b/apps/assets/templates/assets/asset_create.html index 2860e61c9..ac0e4d744 100644 --- a/apps/assets/templates/assets/asset_create.html +++ b/apps/assets/templates/assets/asset_create.html @@ -14,20 +14,18 @@

{% trans 'Basic' %}

{% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} - {% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %} + {% bootstrap_field form.cluster layout="horizontal" %} + {% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.type layout="horizontal" %} {% bootstrap_field form.env layout="horizontal" %}

{% trans 'Auth' %}

- {% bootstrap_field form.username layout="horizontal" %} - {% bootstrap_field form.password layout="horizontal" %} - {% bootstrap_field form.private_key_file layout="horizontal" %} + {% bootstrap_field form.admin_user layout="horizontal" %}
-

{% trans 'Cluster and group' %}

- {% bootstrap_field form.cluster layout="horizontal" %} +

{% trans 'Group' %}

{% bootstrap_field form.groups layout="horizontal" %}
@@ -50,11 +48,11 @@ {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/asset_detail.html b/apps/assets/templates/assets/asset_detail.html index 9fd887cb9..78b21023b 100644 --- a/apps/assets/templates/assets/asset_detail.html +++ b/apps/assets/templates/assets/asset_detail.html @@ -19,9 +19,6 @@
  • {% trans 'Asset detail' %}
  • -
  • - {% trans 'Auth' %} -
  • {% if user.is_superuser %}
  • Update @@ -198,14 +195,6 @@ - - {% trans 'Reset auth' %}: - - - - - - @@ -239,7 +228,7 @@ {{ asset_group.name }} - + {% endfor %} @@ -274,7 +263,7 @@ function updateAssetGroups(groups) { $('#add-asset2group tbody').append( '' + '' + group_name + '' + - '' + + '' + '' ) }); @@ -288,44 +277,20 @@ function updateAssetGroups(groups) { }); } -function updateAssetSystemUser(system_users) { - var the_url = "{% url 'api-assets:asset-update-system-users' pk=asset.id %}"; - var body = { - system_users: Object.assign([], system_users) - }; - var success = function(data) { - $('.select2-selection__rendered').empty(); - $('#groups_selected').val(''); - $.map(jumpserver.system_user_selected, function(name, index) { - $('#opt_' + index).remove(); - - $('#add-asset2systemuser tbody').append( - '' + - '' + name + '' + - '' + - '' - ) - }); - // clear jumpserver.groups_selected - jumpserver.system_user_selected = {}; - }; - APIUpdateAttr({ - url: the_url, - body: JSON.stringify(body), - success: success - }); -} - function refreshAssetHardware() { var the_url = "{% url 'api-assets:asset-refresh' pk=asset.id %}"; var success = function (data) { location.reload(); }; + var error = function (data) { + alert(data) + }; APIUpdateAttr({ url: the_url, success: success, + error: error, method: 'GET' - }) + }); } @@ -337,13 +302,6 @@ $(document).ready(function () { var data = evt.params.data; delete jumpserver.groups_selected[data.id] }); - $('.select2.system-user').select2().on('select2:select', function(evt) { - var data = evt.params.data; - jumpserver.system_user_selected[data.id] = data.text; - }).on('select2:unselect', function(evt) { - var data = evt.params.data; - delete jumpserver.system_user_selected[data.id] - }) }).on('click', '#is_active', function () { var the_url = '{% url "api-assets:asset-detail" pk=asset.id %}'; var checked = $(this).prop('checked'); @@ -370,11 +328,11 @@ $(document).ready(function () { return $(this).data('gid'); }).get(); $.map(jumpserver.groups_selected, function(value, index) { - groups.push(parseInt(index)); + groups.push(index); $('#opt_' + index).remove(); }); updateAssetGroups(groups) -}).on('click', '.btn_leave_group', function() { +}).on('click', '.btn-leave-group', function() { var $this = $(this); var $tr = $this.closest('tr'); var $badge = $tr.find('.bdg_group'); @@ -388,34 +346,6 @@ $(document).ready(function () { return $(this).data('gid'); }).get(); updateAssetGroups(groups) -}).on('click', '.btn-system-user', function () { - if (Object.keys(jumpserver.system_user_selected).length === 0) { - return false; - } - var system_users = $('.bdg_group').map(function() { - return $(this).data('sid'); - }).get(); - $.map(jumpserver.system_user_selected, function(value, index) { - system_users.push(parseInt(index)); - $('#opt_' + index).remove(); - }); - updateAssetSystemUser(system_users) - -}).on('click', '.btn_leave_system', function () { - var $this = $(this); - var $tr = $this.closest('tr'); - var $badge = $tr.find('.bdg_group'); - var sid = $badge.data('sid'); - var name = $badge.html() || $badge.text(); - $('#groups_selected').append( - '' - ); - $tr.remove(); - var system_users = $('.bdg_group').map(function () { - return $(this).data('sid'); - }).get(); - updateAssetSystemUser(system_users) - }).on('click', '.btn-delete-asset', function () { var $this = $(this); var name = "{{ asset.hostname }}"; @@ -424,7 +354,7 @@ $(document).ready(function () { var redirect_url = "{% url 'assets:asset-list' %}"; objectDelete($this, name, the_url, redirect_url); }).on('click', '#btn_refresh_asset', function () { - alert('请等待几秒, 等待完成'); + alert('关闭alert, 等待完成, 自动刷新页面'); refreshAssetHardware() }).on('click', '#btn_test_admin_user', function () { $.ajax({ @@ -436,6 +366,5 @@ $(document).ready(function () { }) }) - {% endblock %} diff --git a/apps/assets/templates/assets/asset_group_detail.html b/apps/assets/templates/assets/asset_group_detail.html index b6639f40d..a21f5a744 100644 --- a/apps/assets/templates/assets/asset_group_detail.html +++ b/apps/assets/templates/assets/asset_group_detail.html @@ -67,16 +67,16 @@
    - + {% for asset in assets_remain %} + {% endfor %} - +
    @@ -93,28 +93,30 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/templates/assets/asset_update.html b/apps/assets/templates/assets/asset_update.html index 4dddf3f38..af5f46566 100644 --- a/apps/assets/templates/assets/asset_update.html +++ b/apps/assets/templates/assets/asset_update.html @@ -19,13 +19,18 @@

    {% trans 'Basic' %}

    {% bootstrap_field form.hostname layout="horizontal" %} {% bootstrap_field form.ip layout="horizontal" %} - {% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.port layout="horizontal" %} + {% bootstrap_field form.cluster layout="horizontal" %} + {% bootstrap_field form.public_ip layout="horizontal" %} {% bootstrap_field form.type layout="horizontal" %} + {% bootstrap_field form.env layout="horizontal" %}
    -

    {% trans 'Cluster and group' %}

    - {% bootstrap_field form.cluster layout="horizontal" %} +

    {% trans 'Auth' %}

    + {% bootstrap_field form.admin_user layout="horizontal" %} + +
    +

    {% trans 'Group' %}

    {% bootstrap_field form.groups layout="horizontal" %}
    diff --git a/apps/assets/templates/assets/cluster_create_update.html b/apps/assets/templates/assets/cluster_create_update.html index 14175a4d4..d9c262fb1 100644 --- a/apps/assets/templates/assets/cluster_create_update.html +++ b/apps/assets/templates/assets/cluster_create_update.html @@ -30,30 +30,31 @@
    -
    -
    - {% csrf_token %} -

    基本信息

    - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.admin_user layout="horizontal" %} - {% bootstrap_field form.address layout="horizontal" %} - {% bootstrap_field form.contact layout="horizontal" %} - {% bootstrap_field form.phone layout="horizontal" %} + + {% csrf_token %} +

    {% trans 'Basic' %}

    + {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.address layout="horizontal" %} + {% bootstrap_field form.contact layout="horizontal" %} + {% bootstrap_field form.phone layout="horizontal" %} -
    -

    IP段

    - {% bootstrap_field form.operator layout="horizontal" %} - {% bootstrap_field form.intranet layout="horizontal" %} - {% bootstrap_field form.extranet layout="horizontal" %} +

    {% trans 'Settings' %}

    + {% bootstrap_field form.admin_user layout="horizontal" %} -
    -
    -
    - - -
    +
    +

    {% trans 'Other' %}

    + {% bootstrap_field form.operator layout="horizontal" %} + {% bootstrap_field form.intranet layout="horizontal" %} + {% bootstrap_field form.extranet layout="horizontal" %} + +
    +
    +
    + +
    - +
    +
    diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 171a1d0ef..de0b234bd 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -1,7 +1,6 @@ # coding:utf-8 from django.conf.urls import url from .. import api -from rest_framework import routers from rest_framework_bulk.routes import BulkRouter app_name = 'assets' diff --git a/apps/assets/utils.py b/apps/assets/utils.py index f7efb50ff..e21a2120e 100644 --- a/apps/assets/utils.py +++ b/apps/assets/utils.py @@ -15,3 +15,5 @@ def get_assets_by_hostname_list(hostname_list): def get_system_user_by_name(name): system_user = get_object_or_none(SystemUser, name=name) return system_user + + diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index f72618f54..a774c127a 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -28,6 +28,7 @@ from .. import forms from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser from ..hands import AdminUserRequiredMixin from ..tasks import update_assets_hardware_info +from ..signals import on_asset_created __all__ = [ @@ -73,11 +74,12 @@ class AssetCreateView(AdminUserRequiredMixin, CreateView): success_url = reverse_lazy('assets:asset-list') def form_valid(self, form): - self.asset = asset = form.save() + asset = form.save() asset.created_by = self.request.user.username or 'Admin' asset.date_created = timezone.now() asset.save() - return super(AssetCreateView, self).form_valid(form) + on_asset_created.send(sender=self.__class__, asset=asset) + return super().form_valid(form) def get_context_data(self, **kwargs): context = { @@ -85,11 +87,11 @@ class AssetCreateView(AdminUserRequiredMixin, CreateView): 'action': 'Create asset', } kwargs.update(context) - return super(AssetCreateView, self).get_context_data(**kwargs) + return super().get_context_data(**kwargs) def get_success_url(self): - update_assets_hardware_info.delay([self.asset._to_secret_json()]) - return super(AssetCreateView, self).get_success_url() + # update_assets_hardware_info.delay([self.asset._to_secret_json()]) + return super().get_success_url() class AssetModalListView(AdminUserRequiredMixin, ListView): diff --git a/apps/assets/views/group.py b/apps/assets/views/group.py index 679e71283..414380522 100644 --- a/apps/assets/views/group.py +++ b/apps/assets/views/group.py @@ -66,20 +66,15 @@ class AssetGroupDetailView(AdminUserRequiredMixin, DetailView): context_object_name = 'asset_group' def get_context_data(self, **kwargs): - assets_remain = Asset.objects.exclude(id__in=self.object.assets.all()) - system_users = SystemUser.objects.all() - system_users_remain = SystemUser.objects.exclude(id__in=system_users) + assets_remain = Asset.objects.exclude(groups__in=[self.object]) context = { 'app': _('Assets'), 'action': _('Asset group detail'), 'assets_remain': assets_remain, - 'assets': [asset for asset in Asset.objects.all() - if asset not in assets_remain], - 'system_users': system_users, - 'system_users_remain': system_users_remain, + 'assets': self.object.assets.all(), } kwargs.update(context) - return super(AssetGroupDetailView, self).get_context_data(**kwargs) + return super().get_context_data(**kwargs) class AssetGroupUpdateView(AdminUserRequiredMixin, UpdateView): diff --git a/apps/common/celery.py b/apps/common/celery.py index 98270a12c..ebf1a549e 100644 --- a/apps/common/celery.py +++ b/apps/common/celery.py @@ -17,8 +17,7 @@ app = Celery('jumpserver') # Using a string here means the worker will not have to # pickle the object when using Windows. app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: [app_config.split('.')[0] - for app_config in settings.INSTALLED_APPS]) +app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS]) app.conf.update( CELERYBEAT_SCHEDULE={ diff --git a/apps/common/mixins.py b/apps/common/mixins.py index f32bbd68b..764d8e50a 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -1,11 +1,13 @@ # coding: utf-8 +import inspect from django.db import models from django.http import JsonResponse from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ + class NoDeleteQuerySet(models.query.QuerySet): def delete(self): @@ -58,3 +60,31 @@ class IDInFilterMixin(object): if isinstance(ids, list): queryset = queryset.filter(id__in=ids) return queryset + + +class BulkSerializerMixin(object): + """ + Become rest_framework_bulk not support uuid as a primary key + so rewrite it. https://github.com/miki725/django-rest-framework-bulk/issues/66 + """ + def to_internal_value(self, data): + from rest_framework_bulk import BulkListSerializer + ret = super(BulkSerializerMixin, self).to_internal_value(data) + + id_attr = getattr(self.Meta, 'update_lookup_field', 'id') + request_method = getattr(getattr(self.context.get('view'), 'request'), 'method', '') + # add update_lookup_field field back to validated data + # since super by default strips out read-only fields + # hence id will no longer be present in validated_data + if all((isinstance(self.root, BulkListSerializer), + id_attr, + request_method in ('PUT', 'PATCH'))): + id_field = self.fields[id_attr] + if data.get("id"): + id_value = id_field.to_internal_value(data.get("id")) + else: + id_value = id_field.to_internal_value(data.get("pk")) + ret[id_attr] = id_value + + return ret + diff --git a/apps/common/tasks.py b/apps/common/tasks.py index 11d0d711d..4e6e33fc4 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -1,9 +1,10 @@ -from __future__ import absolute_import - -# from celery import shared_task from django.core.mail import send_mail from django.conf import settings from common import celery_app as app +from .utils import get_logger + + +logger = get_logger(__file__) @app.task @@ -26,4 +27,7 @@ def send_mail_async(*args, **kwargs): args.insert(2, settings.EMAIL_HOST_USER) args = tuple(args) - send_mail(*args, **kwargs) + try: + send_mail(*args, **kwargs) + except Exception as e: + logger.error("Sending mail error: {}".format(e)) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 596563981..d464adc92 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -331,20 +331,16 @@ BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/3' % { 'host': CONFIG.REDIS_HOST or '127.0.0.1', 'port': CONFIG.REDIS_PORT or 6379, } +CELERY_TASK_SERIALIZER = 'pickle' +CELERY_RESULT_SERIALIZER = 'pickle' CELERY_RESULT_BACKEND = BROKER_URL - +CELERY_ACCEPT_CONTENT = ['json', 'pickle'] +CELERY_TASK_RESULT_EXPIRES = 3600 +CELERYD_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' +CELERYD_TASK_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' +CELERY_TIMEZONE = TIME_ZONE # TERMINAL_HEATBEAT_INTERVAL = CONFIG.TERMINAL_HEATBEAT_INTERVAL or 30 -# crontab job -# CELERYBEAT_SCHEDULE = { -# Check applications is alive every 10m -# 'check_terminal_alive': { -# 'task': 'applications.tasks.check_terminal_alive', -# 'schedule': timedelta(seconds=TERMINAL_HEATBEAT_INTERVAL), -# 'args': (), -# }, -# } - # Cache use redis CACHES = { diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 1bace8351..61feca1e4 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -21,6 +21,7 @@ def is_uuid(s): return False + def record_adhoc(func): def _deco(adhoc, **options): record = AdHocRunHistory(adhoc=adhoc) diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 360c4ad3e..e8840e9a1 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -156,7 +156,7 @@ function activeNav() { function APIUpdateAttr(props) { // props = {url: .., body: , success: , error: , method: ,} props = props || {}; - var success_message = props.success_message || 'Update Successfully!'; + var success_message = props.success_message || 'Update successfully!'; var fail_message = props.fail_message || 'Error occurred while updating.'; $.ajax({ url: props.url, @@ -169,11 +169,10 @@ function APIUpdateAttr(props) { if (typeof props.success === 'function') { return props.success(data); } - - }).fail(function(jqXHR, textStatue, errorThrown) { + }).fail(function(jqXHR, textStatus, errorThrown) { toastr.error(fail_message); if (typeof props.error === 'function') { - return props.error(errorThrown); + return props.error(jqXHR.responseText); } }); // return true; @@ -265,7 +264,7 @@ jumpserver.initDataTable = function (options) { language: { url: options.i18n_url || "/static/js/plugins/dataTables/i18n/zh-hans.json" }, - order: options.order || [[ 1, 'asc' ]], + order: options.order || [], select: options.select || 'multi', buttons: [], columnDefs: columnDefs, diff --git a/apps/templates/_message.html b/apps/templates/_message.html index 8eb061661..6f0811ae2 100644 --- a/apps/templates/_message.html +++ b/apps/templates/_message.html @@ -1,6 +1,6 @@ {% if messages %} {% for message in messages %} -
    +
    {{ message|safe }}
    {% endfor %} diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py index aae1c57b5..53e0c69ae 100644 --- a/apps/terminal/apps.py +++ b/apps/terminal/apps.py @@ -5,3 +5,8 @@ from django.apps import AppConfig class ApplicationsConfig(AppConfig): name = 'terminal' + + def ready(self): + from .signals import on_app_ready + on_app_ready.send(self.__class__) + super().ready() diff --git a/apps/terminal/const.py b/apps/terminal/const.py new file mode 100644 index 000000000..2d74e00c1 --- /dev/null +++ b/apps/terminal/const.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# + +ASSETS_CACHE_KEY = "terminal__session__assets" +USERS_CACHE_KEY = "terminal__session__users" +SYSTEM_USER_CACHE_KEY = "terminal__session__system_users" + diff --git a/apps/terminal/signals.py b/apps/terminal/signals.py new file mode 100644 index 000000000..832a51cd8 --- /dev/null +++ b/apps/terminal/signals.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# +import threading +import time + +from django.core.cache import cache +from django.dispatch import Signal, receiver +from django.db.utils import ProgrammingError, OperationalError + +from common.utils import get_logger +from .const import ASSETS_CACHE_KEY, USERS_CACHE_KEY, SYSTEM_USER_CACHE_KEY + +RUNNING = False +logger = get_logger(__file__) + +on_app_ready = Signal() + + +@receiver(on_app_ready) +def on_app_ready_set_cache(sender, **kwargs): + from .utils import get_session_asset_list, get_session_user_list, \ + get_session_system_user_list + global RUNNING + + def set_cache(): + while True: + try: + assets = get_session_asset_list() + users = get_session_user_list() + system_users = get_session_system_user_list() + + cache.set(ASSETS_CACHE_KEY, assets) + cache.set(USERS_CACHE_KEY, users) + cache.set(SYSTEM_USER_CACHE_KEY, system_users) + except (ProgrammingError, OperationalError): + pass + finally: + time.sleep(10) + if RUNNING: + return + threads = [] + thread = threading.Thread(target=set_cache) + threads.append(thread) + + logger.debug("Receive app ready signal: set cache task start") + for t in threads: + t.daemon = True + t.start() + RUNNING = True diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index c286b1db7..87a90bfcb 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -1,19 +1,15 @@ # -*- coding: utf-8 -*- # -import threading import time from celery import shared_task from django.core.cache import cache -from django.db.utils import ProgrammingError +from django.db.utils import ProgrammingError, OperationalError from .models import Session -ASSETS_CACHE_KEY = "terminal__session__assets" -USERS_CACHE_KEY = "terminal__session__users" -SYSTEM_USER_CACHE_KEY = "terminal__session__system_users" CACHE_REFRESH_INTERVAL = 10 RUNNING = False @@ -24,46 +20,7 @@ def clean_terminal_history(): pass -def get_session_asset_list(): - return set(list(Session.objects.values_list('asset', flat=True))) -def get_session_user_list(): - return set(list(Session.objects.values_list('user', flat=True))) -def get_session_system_user_list(): - return set(list(Session.objects.values_list('system_user', flat=True))) - - -def set_cache(): - while True: - try: - assets = get_session_asset_list() - users = get_session_user_list() - system_users = get_session_system_user_list() - - cache.set(ASSETS_CACHE_KEY, assets) - cache.set(USERS_CACHE_KEY, users) - cache.set(SYSTEM_USER_CACHE_KEY, system_users) - except ProgrammingError: - pass - finally: - time.sleep(10) - - -def main(): - global RUNNING - if RUNNING: - return - threads = [] - thread = threading.Thread(target=set_cache) - threads.append(thread) - - for t in threads: - t.daemon = True - t.start() - RUNNING = True - -# Todo: 不能migrations了 -main() diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index c24571c77..e81ea0e3b 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -1,8 +1,23 @@ # -*- coding: utf-8 -*- # from django.core.cache import cache +from .models import Session + +from .const import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY + + +def get_session_asset_list(): + return set(list(Session.objects.values_list('asset', flat=True))) + + +def get_session_user_list(): + return set(list(Session.objects.values_list('user', flat=True))) + + +def get_session_system_user_list(): + return set(list(Session.objects.values_list('system_user', flat=True))) + -from .tasks import USERS_CACHE_KEY, ASSETS_CACHE_KEY, SYSTEM_USER_CACHE_KEY def get_user_list_from_cache(): diff --git a/apps/users/api.py b/apps/users/api.py index 7ec00b836..160c19028 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -4,11 +4,11 @@ from rest_framework import generics from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework import viewsets from rest_framework_bulk import BulkModelViewSet -from django_filters.rest_framework import DjangoFilterBackend -from . import serializers +from .serializers import UserSerializer, UserGroupSerializer, \ + UserGroupUpdateMemeberSerializer, UserPKUpdateSerializer, \ + UserUpdateGroupSerializer from .tasks import write_login_log_async from .models import User, UserGroup from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly @@ -20,49 +20,23 @@ from common.utils import get_logger logger = get_logger(__name__) -# class UserListView(generics.ListAPIView): -# queryset = User.objects.all() -# serializer_class = serializers.UserSerializer -# filter_fields = ('username', 'email', 'name', 'id') - - -class UserViewSet(viewsets.ModelViewSet): - # class UserViewSet(IDInFilterMixin, BulkModelViewSet): - """ - retrieve: - Return a user instance . - - list: - Return all users except app user, ordered by most recently joined. - - create: - Create a new user. - - delete: - Remove an existing user. - - partial_update: - Update one or more fields on an existing user. - - update: - Update a user. - """ +class UserViewSet(BulkModelViewSet): queryset = User.objects.all() # queryset = User.objects.all().exclude(role="App").order_by("date_joined") - serializer_class = serializers.UserSerializer + serializer_class = UserSerializer permission_classes = (IsSuperUser,) filter_fields = ('username', 'email', 'name', 'id') class UserUpdateGroupApi(generics.RetrieveUpdateAPIView): queryset = User.objects.all() - serializer_class = serializers.UserUpdateGroupSerializer + serializer_class = UserUpdateGroupSerializer permission_classes = (IsSuperUser,) class UserResetPasswordApi(generics.UpdateAPIView): queryset = User.objects.all() - serializer_class = serializers.UserSerializer + serializer_class = UserSerializer def perform_update(self, serializer): # Note: we are not updating the user object here. @@ -77,7 +51,7 @@ class UserResetPasswordApi(generics.UpdateAPIView): class UserResetPKApi(generics.UpdateAPIView): queryset = User.objects.all() - serializer_class = serializers.UserSerializer + serializer_class = UserSerializer def perform_update(self, serializer): from .utils import send_reset_ssh_key_mail @@ -89,7 +63,7 @@ class UserResetPKApi(generics.UpdateAPIView): class UserUpdatePKApi(generics.UpdateAPIView): queryset = User.objects.all() - serializer_class = serializers.UserPKUpdateSerializer + serializer_class = UserPKUpdateSerializer permission_classes = (IsCurrentUserOrReadOnly,) def perform_update(self, serializer): @@ -100,12 +74,12 @@ class UserUpdatePKApi(generics.UpdateAPIView): class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): queryset = UserGroup.objects.all() - serializer_class = serializers.UserGroupSerializer + serializer_class = UserGroupSerializer class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView): queryset = UserGroup.objects.all() - serializer_class = serializers.UserGroupUpdateMemeberSerializer + serializer_class = UserGroupUpdateMemeberSerializer permission_classes = (IsSuperUser,) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 47d701006..1c60ed710 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -189,7 +189,7 @@ class User(AbstractUser): return 'https://www.gravatar.com/avatar/c6812ab450230979465d7bf288eadce2a?s=120&d=identicon' def generate_reset_token(self): - return signer.sign_t({'reset': self.id, 'email': self.email}, expires_in=3600) + return signer.sign_t({'reset': str(self.id), 'email': self.email}, expires_in=3600) def to_json(self): return OrderedDict({ diff --git a/apps/users/serializers.py b/apps/users/serializers.py index fd52d99c7..f8e1d6044 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -3,9 +3,10 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin +from rest_framework_bulk import BulkListSerializer from common.utils import signer, validate_ssh_public_key +from common.mixins import BulkSerializerMixin from .models import User, UserGroup diff --git a/apps/users/signals.py b/apps/users/signals.py new file mode 100644 index 000000000..15bd56c55 --- /dev/null +++ b/apps/users/signals.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# + +from django.dispatch import Signal, receiver + +from common.utils import get_logger + +logger = get_logger(__file__) +on_user_created = Signal(providing_args=['user']) + + +@receiver(on_user_created) +def send_user_add_mail_to_user(sender, user=None, **kwargs): + from .utils import send_user_created_mail + logger.debug("Receive asset create signal, update asset hardware info") + send_user_created_mail(user) + diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index b8196434f..19081026a 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -23,17 +23,18 @@ urlpatterns = [ # User view url(r'^user$', views.UserListView.as_view(), name='user-list'), - url(r'^user/(?P[0-9a-zA-Z\-]+)$', views.UserDetailView.as_view(), name='user-detail'), - url(r'^user/(?P[0-9a-zA-Z\-]+)/asset-permission$', views.UserAssetPermissionView.as_view(), name='user-asset-permission'), - url(r'^user/(?P[0-9a-zA-Z\-]+)/asset-permission/create$', views.UserAssetPermissionCreateView.as_view(), name='user-asset-permission-create'), - url(r'^user/(?P[0-9a-zA-Z\-]+)/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), - url(r'^user/(?P[0-9a-zA-Z\-]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'), url(r'^user/export/', views.UserExportView.as_view(), name='user-export'), url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), url(r'^user/import/$', views.UserBulkImportView.as_view(), name='user-import'), url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'), url(r'^user/(?P[0-9a-zA-Z\-]{36})/update$', views.UserUpdateView.as_view(), name='user-update'), url(r'^user/update$', views.UserBulkUpdateView.as_view(), name='user-bulk-update'), + url(r'^user/(?P[0-9a-zA-Z\-]+)$', views.UserDetailView.as_view(), name='user-detail'), + url(r'^user/(?P[0-9a-zA-Z\-]+)/asset-permission$', views.UserAssetPermissionView.as_view(), name='user-asset-permission'), + url(r'^user/(?P[0-9a-zA-Z\-]+)/asset-permission/create$', views.UserAssetPermissionCreateView.as_view(), name='user-asset-permission-create'), + url(r'^user/(?P[0-9a-zA-Z\-]+)/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), + url(r'^user/(?P[0-9a-zA-Z\-]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'), + # User group view url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'), diff --git a/apps/users/utils.py b/apps/users/utils.py index f5abf658f..02ab846f5 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -31,7 +31,7 @@ class AdminUserRequiredMixin(UserPassesTestMixin): return True -def user_add_success_next(user): +def send_user_created_mail(user): subject = _('Create account successfully') recipient_list = [user.email] message = _(""" @@ -58,6 +58,8 @@ def user_add_success_next(user): 'email': user.email, 'login_url': reverse('users:login', external=True), } + if settings.DEBUG: + print(message) send_mail_async.delay(subject, message, recipient_list, html_message=message) diff --git a/apps/users/views/user.py b/apps/users/views/user.py index e853c230e..b65c0c223 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -29,7 +29,8 @@ from django.contrib.auth import logout as auth_logout from .. import forms from ..models import User, UserGroup -from ..utils import AdminUserRequiredMixin, user_add_success_next +from ..utils import AdminUserRequiredMixin, send_user_created_mail +from ..signals import on_user_created from common.mixins import JSONResponseMixin from common.utils import get_logger, get_object_or_none from perms.models import AssetPermission @@ -74,7 +75,7 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): user = form.save(commit=False) user.created_by = self.request.user.username or 'System' user.save() - user_add_success_next(user) + on_user_created.send(self.__class__, user=user) return super(UserCreateView, self).form_valid(form) def get_success_message(self, cleaned_data): @@ -281,7 +282,7 @@ class UserBulkImportView(AdminUserRequiredMixin, JSONResponseMixin, FormView): user = User.objects.create(**user_dict) user.groups.set(groups) created.append(user_dict['username']) - user_add_success_next(user) + on_user_created.send(self.__class__, user=user) except Exception as e: failed.append('%s: %s' % (user_dict['username'], str(e))) else: