天天看點

從0到1開發自動化運維平台-使用者子產品管理

作者:CharlesLai89

公共模型

建立檔案common/extends/models.py,将cmdb/models.py裡定義的TimeAbstract、CommonParent移到這裡

from django.db import models


class TimeAbstract(models.Model):
    update_time = models.DateTimeField(
        auto_now=True, null=True, blank=True, verbose_name='更新時間')
    created_time = models.DateTimeField(
        auto_now_add=True, null=True, blank=True, verbose_name='建立時間')

    class ExtMeta:
        related = False
        dashboard = False

    class Meta:
        abstract = True
        ordering = ['-id']


class CommonParent(models.Model):
    parent = models.ForeignKey(
        "self", null=True, blank=True, on_delete=models.SET_NULL, related_name='children')

    class Meta:
        abstract = True           

建立使用者子產品

建立使用者中心子產品

(venv) ➜ ydevops-backend django-admin startapp ucenter
(venv) ➜ ydevops-backend mv ucenter apps           

編寫使用者組織架構及rbac模型

from django.db import models
from django.contrib.auth.models import AbstractUser

from common.extends.models import TimeAbstract, CommonParent

# Create your models here.
def org_extra_data():
    return {
        'leader_user_id': '', # 存儲部門上司ID
        'dn': '', # 存儲ldap dn
    }


def user_extra_data():
    return {
        'ding_userid': '', # 釘釘使用者ID
        'feishu_userid': '', # 飛書UserID
        'feishu_unionid': '', # 飛書UnionID
        'feishu_openid': '', # 飛書OpenID
        'leader_user_id': '', # 直屬上司ID
        'dn': '', # ldap dn
    }


class Menu(TimeAbstract, CommonParent):
    """
    菜單模型
    """
    name = models.CharField(max_length=30, unique=True, verbose_name='菜單名')
    title = models.CharField(max_length=30, null=True, blank=True, verbose_name='菜單顯示名')
    icon = models.CharField(max_length=50, null=True, blank=True, verbose_name='圖示')
    path = models.CharField(max_length=158, null=True, blank=True, verbose_name='路由位址')
    redirect = models.CharField(max_length=200, null=True, blank=True, verbose_name='跳轉位址')
    is_frame = models.BooleanField(default=False, verbose_name='外部菜單')
    hidden = models.BooleanField(default=False, verbose_name='是否隐藏')
    spread = models.BooleanField(default=False, verbose_name='是否預設展開')
    sort = models.IntegerField(default=0, verbose_name='排序标記')
    component = models.CharField(max_length=200, default='Layout', verbose_name='元件')
    affix = models.BooleanField(default=False, verbose_name='固定标簽')
    single = models.BooleanField(default=False, verbose_name='标簽單開')
    activeMenu = models.CharField(max_length=128, blank=True, null=True, verbose_name='激活菜單')

    def __str__(self):
        return self.name

    class Meta:
        default_permissions = ()
        verbose_name = '菜單'
        verbose_name_plural = verbose_name + '管理'
        ordering = ['sort', 'name']


class Permission(TimeAbstract, CommonParent):
    """
    權限模型
    """
    name = models.CharField(max_length=30, unique=True, verbose_name='權限名')
    method = models.CharField(max_length=50, null=True, blank=True, verbose_name='方法')

    def __str__(self):
        return self.name

    class Meta:
        default_permissions = ()
        verbose_name = '權限'
        verbose_name_plural = verbose_name + '管理'


class Role(TimeAbstract):
    """
    角色模型
    """
    name = models.CharField(max_length=32, unique=True, verbose_name='角色')
    permissions = models.ManyToManyField(Permission, blank=True, related_name='role_permission', verbose_name='權限')
    menus = models.ManyToManyField(Menu, blank=True, verbose_name='菜單')
    desc = models.CharField(max_length=50, blank=True, null=True, verbose_name='描述')

    def __str__(self):
        return self.name

    class Meta:
        default_permissions = ()
        verbose_name = '角色'
        verbose_name_plural = verbose_name + '管理'


class Organization(TimeAbstract, CommonParent):
    """
    組織架構
    """
    organization_type_choices = (
        ('company', '公司'),
        ('department', '部門')
    )
    dept_id = models.CharField(max_length=32, unique=True, verbose_name='部門ID')
    name = models.CharField(max_length=60, verbose_name='名稱')
    type = models.CharField(max_length=20, choices=organization_type_choices, default='department', verbose_name='類型')
    extra_data = models.JSONField(default=org_extra_data, verbose_name='其它資料', help_text=f'資料格式:{org_extra_data()}')

    @property
    def full(self):
        l = []
        self.get_parents(l)
        return l

    def get_parents(self, parent_result: list):
        if not parent_result:
            parent_result.append(self)
        parent_obj = self.parent
        if parent_obj:
            parent_result.append(parent_obj)
            parent_obj.get_parents(parent_result)

    def __str__(self):
        return self.name

    class ExtMeta:
        related = True
        dashboard = False

    class Meta:
        default_permissions = ()
        verbose_name = '組織架構'
        verbose_name_plural = verbose_name + '管理'


class UserProfile(TimeAbstract, AbstractUser):
    """
    使用者資訊
    """
    mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name='手機号碼')
    avatar = models.ImageField(upload_to='static/%Y/%m', default='image/default.png',
                               max_length=250, null=True, blank=True)
    department = models.ManyToManyField(Organization, related_name='org_user', verbose_name='部門')
    # 職能:根據職能授權
    position = models.CharField(max_length=50, null=True, blank=True, verbose_name='職能')
    # 職位:僅展示使用者title資訊
    title = models.CharField(max_length=50, null=True, blank=True, verbose_name='職位')
    roles = models.ManyToManyField(Role, verbose_name='角色', related_name='user_role', blank=True)
    extra_data = models.JSONField(default=user_extra_data, verbose_name='其它資料', help_text=f'資料格式:{user_extra_data()}')
    is_ldap = models.BooleanField(default=False, verbose_name='是否ldap使用者')

    @property
    def name(self):
        if self.first_name:
            return self.first_name
        return self.username

    def __str__(self):
        return self.name

    class ExtMeta:
        related = True
        dashboard = False
        icon = 'peoples'

    class Meta:
        default_permissions = ()
        verbose_name = '使用者資訊'
        verbose_name_plural = verbose_name + '管理'
        ordering = ['id']           

安裝依賴

pip install pillow           

添加子產品到settings.py及配置自定義的使用者認證模型

INSTALLED_APPS = [
...
'ucenter.apps.UcenterConfig',
]
...

AUTH_USER_MODEL = 'ucenter.UserProfile'           

CMDB模型更新

調整cmdb子產品裡的模型

# 注釋原有使用者模型,替換為自定義的模型
# from django.contrib.auth.models import User 
from ucenter.models import UserProfile as User
from .model_assets import Idc, Region
# 從common裡導入TimeAbstract, CommonParent
from common.extends.models import TimeAbstract, CommonParent           

遷移資料

(venv) ➜  ydevops-backend python manage.py makemigrations
Migrations for 'ucenter':
  apps/ucenter/migrations/0001_initial.py
    - Create model Menu
    - Create model Permission
    - Create model Role
    - Create model Organization
    - Create model UserProfile           

此時建立表時會有異常

(venv) ➜  ydevops-backend python manage.py migrate
...
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency ucenter.0001_initial on database 'default'.           

我們現在開發階段,最省事的就是直接把database删除,重新生成

rm db.sqlite3
python manage.py makemigrations
python manage.py migrate           

當然,如果想嘗試解決,我們按如下步驟

  1. 注釋settings.py->INSTAALLLED_APPS裡的'django.contrib.admin';注釋urls.py->urlpatterns裡的path('admin/', admin.site.urls)。admin是django自帶的管理背景,我們可以不用這個,直接删除也行......
  2. 删除cmdb裡的遷移檔案apps/cmdb/migrations/0*.py
  3. 建立ucenter
python manage.py migrate ucenter           
  1. 重新生成遷移檔案
python manage.py makemigrations           
  1. 檢視遷移,確定所有檔案已執行完成
python manage.py showmigrations           

由于我們使用了自定義的使用者模型,需要重新建立使用者

(venv) ➜ ydevops-backend python manage.py createsuperuser           

編寫序列化器

class UserProfileListSerializers(serializers.ModelSerializer):
    user_department = serializers.SerializerMethodField()
    user_director = serializers.SerializerMethodField()

    def get_user_department(self, instance):
        return [{'org_id': i.id, 'org_name': i.name} for i in instance.department.all()]

    def get_user_director(self, instance):
        leader_ou = [i.extra_data['leader_user_id'] for i in instance.department.all() if i.extra_data.get('leader_user_id', None)]
        leaders = UserProfile.objects.filter(extra_data__feishu_openid__in=leader_ou)
        return [[{'id': i.id, 'name': i.name} for i in leaders]]

    class Meta:
        model = UserProfile
        exclude = ('password', 'dn')


class UserProfileDetailSerializers(UserProfileListSerializers):
    user_roles = serializers.SerializerMethodField()
    routers = serializers.SerializerMethodField()
    permissions = serializers.SerializerMethodField()

    def get_user_roles(self, instance):
        try:
            qs = instance.roles.all()
            return [{'id': i.id, 'name': i.name, 'desc': i.desc} for i in qs]
        except BaseException as e:
            return []

    def get_permissions(self, instance):
        perms = instance.roles.values(
            'permissions__method',
        ).distinct()
        if instance.is_superuser:
            return ['admin']
        return [p['permissions__method'] for p in perms if p['permissions__method']]

    def get_routers(self, instance):
        qs = []
        if instance.is_superuser or 'admin' in [p['permissions__method'] for p in
                                                instance.roles.values('permissions__method')]:
            qs = Menu.objects.filter(parent__isnull=True)
            serializer = MenuListSerializers(instance=qs, many=True)
            tree_data = serializer.data
        else:
            [qs.extend(i.menus.all()) for i in instance.roles.all()]
            serializer = UserMenuSerializers(instance=qs, many=True)
            # 組織使用者擁有的菜單清單
            tree_dict = {}
            tree_data = []
            try:
                for item in serializer.data:
                    tree_dict[item['id']] = item
                for i in tree_dict:
                    if tree_dict[i]['parent']:
                        pid = tree_dict[i]['parent']
                        parent = tree_dict[pid]
                        parent.setdefault('children', []).append(tree_dict[i])
                    else:
                        tree_data.append(tree_dict[i])
            except:
                tree_data = serializer.data
        return tree_data

    class Meta:
        model = UserProfile
        exclude = ('avatar',)


class UserProfileSerializers(serializers.ModelSerializer):

    class Meta:
        model = UserProfile
        exclude = ('avatar',)

    def create(self, validated_data):
        roles = validated_data.pop('roles')
        departments = validated_data.pop('department')
        instance = UserProfile.objects.create(**validated_data)
        instance.set_password(validated_data['password'])
        instance.save()
        instance.department.set(departments)
        instance.roles.set(roles)
        return instance
           

使用者管理視圖

import shortuuid
from django.db.models import Q
from django.core.cache import cache
from config import USER_AUTH_BACKEND

import logging

logger = logging.getLogger(__name__)


USER_SYNC_KEY = {
    'feishu': 'celery_job:feishu_user_sync', # 同步飛書組織架構任務key
    'ldap': 'celery_job:ldap_user_sync', # LDAP使用者同步任務KEY
}


class UserViewSet(AutoModelViewSet):
    """
    使用者管理視圖

    ### 使用者管理權限
        {'*': ('user_all', '使用者管理')},
        {'get': ('user_list', '檢視使用者')},
        {'post': ('user_create', '建立使用者')},
        {'put': ('user_edit', '編輯使用者')},
        {'patch': ('user_edit', '編輯使用者')},
        {'delete': ('user_delete', '删除使用者')}
    """
    perms_map = (
        {'*': ('admin', '管理者')},
        {'*': ('user_all', '使用者管理')},
        {'get': ('user_list', '檢視使用者')},
        {'post': ('user_create', '建立使用者')},
        {'put': ('user_edit', '編輯使用者')},
        {'patch': ('user_edit', '編輯使用者')},
        {'delete': ('user_delete', '删除使用者')}
    )
    queryset = UserProfile.objects.exclude(Q(username='thirdparty') | Q(is_active=False))
    serializer_class = UserProfileSerializers
    serializer_list_class = UserProfileListSerializers

    def get_serializer_class(self):
        if self.action in ['detail', 'retrieve']:
            return UserProfileDetailSerializers
        return super().get_serializer_class()

    def create(self, request, *args, **kwargs):
        if self.queryset.filter(username=request.data['username']):
            return ops_response({}, success=False, errorCode=40300, errorMessage='%s 賬号已存在!' % request.data['username'])
        password = shortuuid.ShortUUID().random(length=8)
        request.data['password'] = password
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        data = serializer.data
        data['password'] = password
        data['status'] = 'success'
        data['code'] = 20000
        return ops_response(data)

    def perform_destroy(self, instance):
        # 禁用使用者
        instance.is_active = False
        instance.save()

    @action(methods=['POST'], url_path='password/reset', detail=False)
    def password_reset(self, request):
        """
        重置使用者密碼

        ### 重置使用者密碼
        """
        data = self.request.data
        user = self.queryset.get(pk=data['uid'])
        if user.is_superuser:
            return ops_response({}, success=False, errorCode=40300, errorMessage='禁止修改管理者密碼!')
        user.set_password(data['password'])
        user.save()
        return ops_response('密碼已更新.')

    @action(methods=['GET'], url_path='detail', detail=False)
    def detail_info(self, request, pk=None, *args, **kwargs):
        """
        使用者詳細清單

        ### 擷取使用者詳細資訊,使用者管理子產品
        """
        return super().list(request, pk, *args, **kwargs)

    @action(methods=['POST'], url_path='sync', detail=False)
    def user_sync(self, request):
        """
        使用者同步

        ### 傳遞參數:
            sync: 1
        """
        sync = request.data.get('sync', 0)
        is_job_exist = cache.get(USER_SYNC_KEY[USER_AUTH_BACKEND])
        if is_job_exist:
            return ops_response({}, success=False, errorCode=40300, errorMessage='已經有組織架構同步任務在運作中... 請稍後重新整理頁面檢視')

        if sync:
            # 同步任務,後面再實作
            taskid = None
            # 限制隻能有一個同步任務在跑
            cache.set(USER_SYNC_KEY[USER_AUTH_BACKEND], taskid, timeout=300)
        return ops_response('正在同步組織架構資訊...')
           

添加路由

加使用者管理的路由加到devops_backend/urls.py

...
router.register('users', UserViewSet)           

運作項目

通路http://localhost:9000/apidoc/,輸入users過濾可以看到使用者子產品接口

從0到1開發自動化運維平台-使用者子產品管理

繼續閱讀