mirror of https://github.com/jumpserver/jumpserver
				
				
				
			Merge branch 'v3' of https://github.com/jumpserver/jumpserver into pr@v3@feat_db_automations
						commit
						be875638ed
					
				| 
						 | 
				
			
			@ -6,5 +6,6 @@ from .label import *
 | 
			
		|||
from .account import *
 | 
			
		||||
from .node import *
 | 
			
		||||
from .domain import *
 | 
			
		||||
from .automations import *
 | 
			
		||||
from .gathered_user import *
 | 
			
		||||
from .favorite_asset import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,10 +82,11 @@ class AssetsTaskMixin:
 | 
			
		|||
    def perform_assets_task(self, serializer):
 | 
			
		||||
        data = serializer.validated_data
 | 
			
		||||
        assets = data.get('assets', [])
 | 
			
		||||
        asset_ids = [asset.id for asset in assets]
 | 
			
		||||
        if data['action'] == "refresh":
 | 
			
		||||
            task = update_assets_hardware_info_manual.delay(assets)
 | 
			
		||||
            task = update_assets_hardware_info_manual.delay(asset_ids)
 | 
			
		||||
        else:
 | 
			
		||||
            task = test_assets_connectivity_manual.delay(assets)
 | 
			
		||||
            task = test_assets_connectivity_manual.delay(asset_ids)
 | 
			
		||||
        return task
 | 
			
		||||
 | 
			
		||||
    def perform_create(self, serializer):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from .base import *
 | 
			
		||||
from .change_secret import *
 | 
			
		||||
from .gather_accounts import *
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,118 @@
 | 
			
		|||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework import status, mixins, viewsets
 | 
			
		||||
 | 
			
		||||
from orgs.mixins import generics
 | 
			
		||||
from assets import serializers
 | 
			
		||||
from assets.const import AutomationTypes
 | 
			
		||||
from assets.tasks import execute_automation
 | 
			
		||||
from assets.models import BaseAutomation, AutomationExecution
 | 
			
		||||
from common.const.choices import Trigger
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
 | 
			
		||||
    'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationExecutionViewSet'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationAssetsListApi(generics.ListAPIView):
 | 
			
		||||
    serializer_class = serializers.AutomationAssetsSerializer
 | 
			
		||||
    filter_fields = ("name", "address")
 | 
			
		||||
    search_fields = filter_fields
 | 
			
		||||
 | 
			
		||||
    def get_object(self):
 | 
			
		||||
        pk = self.kwargs.get('pk')
 | 
			
		||||
        return get_object_or_404(BaseAutomation, pk=pk)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        instance = self.get_object()
 | 
			
		||||
        assets = instance.get_all_assets().only(
 | 
			
		||||
            *self.serializer_class.Meta.only_fields
 | 
			
		||||
        )
 | 
			
		||||
        return assets
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    model = BaseAutomation
 | 
			
		||||
    serializer_class = serializers.UpdateAssetSerializer
 | 
			
		||||
 | 
			
		||||
    def update(self, request, *args, **kwargs):
 | 
			
		||||
        instance = self.get_object()
 | 
			
		||||
        serializer = self.serializer_class(data=request.data)
 | 
			
		||||
 | 
			
		||||
        if not serializer.is_valid():
 | 
			
		||||
            return Response({'error': serializer.errors})
 | 
			
		||||
 | 
			
		||||
        assets = serializer.validated_data.get('assets')
 | 
			
		||||
        if assets:
 | 
			
		||||
            instance.assets.remove(*tuple(assets))
 | 
			
		||||
        return Response({'msg': 'ok'})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    model = BaseAutomation
 | 
			
		||||
    serializer_class = serializers.UpdateAssetSerializer
 | 
			
		||||
 | 
			
		||||
    def update(self, request, *args, **kwargs):
 | 
			
		||||
        instance = self.get_object()
 | 
			
		||||
        serializer = self.serializer_class(data=request.data)
 | 
			
		||||
        if serializer.is_valid():
 | 
			
		||||
            assets = serializer.validated_data.get('assets')
 | 
			
		||||
            if assets:
 | 
			
		||||
                instance.assets.add(*tuple(assets))
 | 
			
		||||
            return Response({"msg": "ok"})
 | 
			
		||||
        else:
 | 
			
		||||
            return Response({"error": serializer.errors})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
 | 
			
		||||
    model = BaseAutomation
 | 
			
		||||
    serializer_class = serializers.UpdateAssetSerializer
 | 
			
		||||
 | 
			
		||||
    def update(self, request, *args, **kwargs):
 | 
			
		||||
        action_params = ['add', 'remove']
 | 
			
		||||
        action = request.query_params.get('action')
 | 
			
		||||
        if action not in action_params:
 | 
			
		||||
            err_info = _("The parameter 'action' must be [{}]".format(','.join(action_params)))
 | 
			
		||||
            return Response({"error": err_info})
 | 
			
		||||
 | 
			
		||||
        instance = self.get_object()
 | 
			
		||||
        serializer = self.serializer_class(data=request.data)
 | 
			
		||||
        if serializer.is_valid():
 | 
			
		||||
            nodes = serializer.validated_data.get('nodes')
 | 
			
		||||
            if nodes:
 | 
			
		||||
                # eg: plan.nodes.add(*tuple(assets))
 | 
			
		||||
                getattr(instance.nodes, action)(*tuple(nodes))
 | 
			
		||||
            return Response({"msg": "ok"})
 | 
			
		||||
        else:
 | 
			
		||||
            return Response({"error": serializer.errors})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationExecutionViewSet(
 | 
			
		||||
    mixins.CreateModelMixin, mixins.ListModelMixin,
 | 
			
		||||
    mixins.RetrieveModelMixin, viewsets.GenericViewSet
 | 
			
		||||
):
 | 
			
		||||
    search_fields = ('trigger',)
 | 
			
		||||
    filterset_fields = ('trigger', 'automation_id')
 | 
			
		||||
    serializer_class = serializers.AutomationExecutionSerializer
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = AutomationExecution.objects.all()
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    def filter_queryset(self, queryset):
 | 
			
		||||
        queryset = super().filter_queryset(queryset)
 | 
			
		||||
        queryset = queryset.order_by('-date_start')
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    def create(self, request, *args, **kwargs):
 | 
			
		||||
        serializer = self.get_serializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        automation = serializer.validated_data.get('automation')
 | 
			
		||||
        tp = serializer.validated_data.get('type')
 | 
			
		||||
        model = AutomationTypes.get_type_model(tp)
 | 
			
		||||
        task = execute_automation.delay(
 | 
			
		||||
            pid=automation.pk, trigger=Trigger.manual, model=model
 | 
			
		||||
        )
 | 
			
		||||
        return Response({'task': task.id}, status=status.HTTP_201_CREATED)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
from rest_framework import mixins
 | 
			
		||||
 | 
			
		||||
from common.utils import get_object_or_none
 | 
			
		||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
 | 
			
		||||
 | 
			
		||||
from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
 | 
			
		||||
from assets import serializers
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
 | 
			
		||||
    model = ChangeSecretAutomation
 | 
			
		||||
    filter_fields = ('name', 'secret_type', 'secret_strategy')
 | 
			
		||||
    search_fields = filter_fields
 | 
			
		||||
    ordering_fields = ('name',)
 | 
			
		||||
    serializer_class = serializers.ChangeSecretAutomationSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
 | 
			
		||||
    serializer_class = serializers.ChangeSecretRecordSerializer
 | 
			
		||||
    filter_fields = ['asset', 'execution_id']
 | 
			
		||||
    search_fields = ['asset__hostname']
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return ChangeSecretRecord.objects.all()
 | 
			
		||||
 | 
			
		||||
    def filter_queryset(self, queryset):
 | 
			
		||||
        queryset = super().filter_queryset(queryset)
 | 
			
		||||
        eid = self.request.GET.get('execution_id')
 | 
			
		||||
        execution = get_object_or_none(AutomationExecution, pk=eid)
 | 
			
		||||
        if execution:
 | 
			
		||||
            queryset = queryset.filter(execution=execution)
 | 
			
		||||
        queryset = queryset.order_by('is_success', '-date_start')
 | 
			
		||||
        return queryset
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from orgs.mixins.api import OrgBulkModelViewSet
 | 
			
		||||
 | 
			
		||||
from assets.models import GatherAccountsAutomation
 | 
			
		||||
from assets import serializers
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'GatherAccountsAutomationViewSet',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
 | 
			
		||||
    model = GatherAccountsAutomation
 | 
			
		||||
    filter_fields = ('name',)
 | 
			
		||||
    search_fields = filter_fields
 | 
			
		||||
    ordering_fields = ('name',)
 | 
			
		||||
    serializer_class = serializers.GatherAccountAutomationSerializer
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +47,7 @@ class PushOrVerifyHostCallbackMixin:
 | 
			
		|||
            secret = account.secret
 | 
			
		||||
 | 
			
		||||
            private_key_path = None
 | 
			
		||||
            if account.secret_type == SecretType.ssh_key:
 | 
			
		||||
            if account.secret_type == SecretType.SSH_KEY:
 | 
			
		||||
                private_key_path = self.generate_private_key_path(secret, path_dir)
 | 
			
		||||
                secret = self.generate_public_key(secret)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -221,6 +221,7 @@ class BasePlaybookManager:
 | 
			
		|||
        else:
 | 
			
		||||
            print(">>> 开始执行任务\n")
 | 
			
		||||
 | 
			
		||||
        self.execution.date_start = timezone.now()
 | 
			
		||||
        for i, runner in enumerate(runners, start=1):
 | 
			
		||||
            if len(runners) > 1:
 | 
			
		||||
                print(">>> 开始执行第 {} 批任务".format(i))
 | 
			
		||||
| 
						 | 
				
			
			@ -231,3 +232,6 @@ class BasePlaybookManager:
 | 
			
		|||
            except Exception as e:
 | 
			
		||||
                self.on_runner_failed(runner, e)
 | 
			
		||||
            print('\n')
 | 
			
		||||
        self.execution.status = 'success'
 | 
			
		||||
        self.execution.date_finished = timezone.now()
 | 
			
		||||
        self.execution.save()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -89,9 +89,9 @@ class ChangeSecretManager(BasePlaybookManager):
 | 
			
		|||
            return self.generate_password()
 | 
			
		||||
 | 
			
		||||
    def get_secret(self):
 | 
			
		||||
        if self.secret_type == SecretType.ssh_key:
 | 
			
		||||
        if self.secret_type == SecretType.SSH_KEY:
 | 
			
		||||
            secret = self.get_ssh_key()
 | 
			
		||||
        elif self.secret_type == SecretType.password:
 | 
			
		||||
        elif self.secret_type == SecretType.PASSWORD:
 | 
			
		||||
            secret = self.get_password()
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError("Secret must be set")
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +99,7 @@ class ChangeSecretManager(BasePlaybookManager):
 | 
			
		|||
 | 
			
		||||
    def get_kwargs(self, account, secret):
 | 
			
		||||
        kwargs = {}
 | 
			
		||||
        if self.secret_type != SecretType.ssh_key:
 | 
			
		||||
        if self.secret_type != SecretType.SSH_KEY:
 | 
			
		||||
            return kwargs
 | 
			
		||||
        kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy']
 | 
			
		||||
        kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ class ChangeSecretManager(BasePlaybookManager):
 | 
			
		|||
            self.name_recorder_mapper[h['name']] = recorder
 | 
			
		||||
 | 
			
		||||
            private_key_path = None
 | 
			
		||||
            if self.secret_type == SecretType.ssh_key:
 | 
			
		||||
            if self.secret_type == SecretType.SSH_KEY:
 | 
			
		||||
                private_key_path = self.generate_private_key_path(new_secret, path_dir)
 | 
			
		||||
                new_secret = self.generate_public_key(new_secret)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,18 @@ from .gather_accounts.manager import GatherAccountsManager
 | 
			
		|||
from .verify_account.manager import VerifyAccountManager
 | 
			
		||||
from .push_account.manager import PushAccountManager
 | 
			
		||||
from .backup_account.manager import AccountBackupManager
 | 
			
		||||
from .ping.manager import PingManager
 | 
			
		||||
from ..const import AutomationTypes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExecutionManager:
 | 
			
		||||
    manager_type_mapper = {
 | 
			
		||||
        AutomationTypes.change_secret: ChangeSecretManager,
 | 
			
		||||
        AutomationTypes.gather_facts: GatherFactsManager,
 | 
			
		||||
        AutomationTypes.gather_accounts: GatherAccountsManager,
 | 
			
		||||
        AutomationTypes.verify_account: VerifyAccountManager,
 | 
			
		||||
        AutomationTypes.ping: PingManager,
 | 
			
		||||
        AutomationTypes.push_account: PushAccountManager,
 | 
			
		||||
        AutomationTypes.gather_facts: GatherFactsManager,
 | 
			
		||||
        AutomationTypes.change_secret: ChangeSecretManager,
 | 
			
		||||
        AutomationTypes.verify_account: VerifyAccountManager,
 | 
			
		||||
        AutomationTypes.gather_accounts: GatherAccountsManager,
 | 
			
		||||
        # TODO 后期迁移到自动化策略中
 | 
			
		||||
        'backup_account': AccountBackupManager,
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,14 +21,14 @@ class PingManager(BasePlaybookManager):
 | 
			
		|||
 | 
			
		||||
    def on_host_success(self, host, result):
 | 
			
		||||
        asset, account = self.host_asset_and_account_mapper.get(host)
 | 
			
		||||
        asset.set_connectivity(Connectivity.ok)
 | 
			
		||||
        asset.set_connectivity(Connectivity.OK)
 | 
			
		||||
        if not account:
 | 
			
		||||
            return
 | 
			
		||||
        account.set_connectivity(Connectivity.ok)
 | 
			
		||||
        account.set_connectivity(Connectivity.OK)
 | 
			
		||||
 | 
			
		||||
    def on_host_error(self, host, error, result):
 | 
			
		||||
        asset, account = self.host_asset_and_account_mapper.get(host)
 | 
			
		||||
        asset.set_connectivity(Connectivity.failed)
 | 
			
		||||
        asset.set_connectivity(Connectivity.FAILED)
 | 
			
		||||
        if not account:
 | 
			
		||||
            return
 | 
			
		||||
        account.set_connectivity(Connectivity.failed)
 | 
			
		||||
        account.set_connectivity(Connectivity.FAILED)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,8 +18,8 @@ class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager):
 | 
			
		|||
 | 
			
		||||
    def on_host_success(self, host, result):
 | 
			
		||||
        account = self.host_account_mapper.get(host)
 | 
			
		||||
        account.set_connectivity(Connectivity.ok)
 | 
			
		||||
        account.set_connectivity(Connectivity.OK)
 | 
			
		||||
 | 
			
		||||
    def on_host_error(self, host, error, result):
 | 
			
		||||
        account = self.host_account_mapper.get(host)
 | 
			
		||||
        account.set_connectivity(Connectivity.failed)
 | 
			
		||||
        account.set_connectivity(Connectivity.FAILED)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,13 +3,13 @@ from django.utils.translation import ugettext_lazy as _
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class Connectivity(TextChoices):
 | 
			
		||||
    unknown = 'unknown', _('Unknown')
 | 
			
		||||
    ok = 'ok', _('Ok')
 | 
			
		||||
    failed = 'failed', _('Failed')
 | 
			
		||||
    UNKNOWN = 'unknown', _('Unknown')
 | 
			
		||||
    OK = 'ok', _('Ok')
 | 
			
		||||
    FAILED = 'failed', _('Failed')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SecretType(TextChoices):
 | 
			
		||||
    password = 'password', _('Password')
 | 
			
		||||
    ssh_key = 'ssh_key', _('SSH key')
 | 
			
		||||
    access_key = 'access_key', _('Access key')
 | 
			
		||||
    token = 'token', _('Token')
 | 
			
		||||
    PASSWORD = 'password', _('Password')
 | 
			
		||||
    SSH_KEY = 'ssh_key', _('SSH key')
 | 
			
		||||
    ACCESS_KEY = 'access_key', _('Access key')
 | 
			
		||||
    TOKEN = 'token', _('Token')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,22 @@ class AutomationTypes(TextChoices):
 | 
			
		|||
    verify_account = 'verify_account', _('Verify account')
 | 
			
		||||
    gather_accounts = 'gather_accounts', _('Gather accounts')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_type_model(cls, tp):
 | 
			
		||||
        from assets.models import (
 | 
			
		||||
            PingAutomation, GatherFactsAutomation, PushAccountAutomation,
 | 
			
		||||
            ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation,
 | 
			
		||||
        )
 | 
			
		||||
        type_model_dict = {
 | 
			
		||||
            cls.ping: PingAutomation,
 | 
			
		||||
            cls.gather_facts: GatherFactsAutomation,
 | 
			
		||||
            cls.push_account: PushAccountAutomation,
 | 
			
		||||
            cls.change_secret: ChangeSecretAutomation,
 | 
			
		||||
            cls.verify_account: VerifyAccountAutomation,
 | 
			
		||||
            cls.gather_accounts: GatherAccountsAutomation,
 | 
			
		||||
        }
 | 
			
		||||
        return type_model_dict.get(tp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SecretStrategy(TextChoices):
 | 
			
		||||
    custom = 'specific', _('Specific')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
from .change_secret import *
 | 
			
		||||
from .discovery_account import *
 | 
			
		||||
from .ping import *
 | 
			
		||||
from .base import *
 | 
			
		||||
from .push_account import *
 | 
			
		||||
from .gather_facts import *
 | 
			
		||||
from .gather_accounts import *
 | 
			
		||||
from .change_secret import *
 | 
			
		||||
from .verify_account import *
 | 
			
		||||
from .ping import *
 | 
			
		||||
from .gather_accounts import *
 | 
			
		||||
from .discovery_account import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ from celery import current_task
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from common.const.choices import Trigger, Status
 | 
			
		||||
from common.const.choices import Trigger
 | 
			
		||||
from common.mixins.models import CommonModelMixin
 | 
			
		||||
from common.db.fields import EncryptJsonDictTextField
 | 
			
		||||
from orgs.mixins.models import OrgModelMixin
 | 
			
		||||
| 
						 | 
				
			
			@ -15,12 +15,8 @@ from assets.const import AutomationTypes
 | 
			
		|||
 | 
			
		||||
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
 | 
			
		||||
    accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
 | 
			
		||||
    nodes = models.ManyToManyField(
 | 
			
		||||
        'assets.Node', blank=True, verbose_name=_("Nodes")
 | 
			
		||||
    )
 | 
			
		||||
    assets = models.ManyToManyField(
 | 
			
		||||
        'assets.Asset', blank=True, verbose_name=_("Assets")
 | 
			
		||||
    )
 | 
			
		||||
    nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
 | 
			
		||||
    assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
 | 
			
		||||
    type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
 | 
			
		||||
    is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
 | 
			
		||||
    comment = models.TextField(blank=True, verbose_name=_('Comment'))
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +88,7 @@ class AutomationExecution(OrgModelMixin):
 | 
			
		|||
        'BaseAutomation', related_name='executions', on_delete=models.CASCADE,
 | 
			
		||||
        verbose_name=_('Automation task')
 | 
			
		||||
    )
 | 
			
		||||
    status = models.CharField(max_length=16, default='pending')
 | 
			
		||||
    status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
 | 
			
		||||
    date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
 | 
			
		||||
    date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
 | 
			
		||||
    date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
 | 
			
		|||
class ChangeSecretAutomation(BaseAutomation):
 | 
			
		||||
    secret_type = models.CharField(
 | 
			
		||||
        choices=SecretType.choices, max_length=16,
 | 
			
		||||
        default=SecretType.password, verbose_name=_('Secret type')
 | 
			
		||||
        default=SecretType.PASSWORD, verbose_name=_('Secret type')
 | 
			
		||||
    )
 | 
			
		||||
    secret_strategy = models.CharField(
 | 
			
		||||
        choices=SecretStrategy.choices, max_length=16,
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ class ChangeSecretAutomation(BaseAutomation):
 | 
			
		|||
        choices=SSHKeyStrategy.choices, max_length=16,
 | 
			
		||||
        default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
 | 
			
		||||
    )
 | 
			
		||||
    recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient"))
 | 
			
		||||
    recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        self.type = AutomationTypes.change_secret
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,3 +13,7 @@ class GatherAccountsAutomation(BaseAutomation):
 | 
			
		|||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("Gather asset accounts")
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def executed_amount(self):
 | 
			
		||||
        return self.executions.count()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ logger = get_logger(__file__)
 | 
			
		|||
 | 
			
		||||
class AbsConnectivity(models.Model):
 | 
			
		||||
    connectivity = models.CharField(
 | 
			
		||||
        choices=Connectivity.choices, default=Connectivity.unknown,
 | 
			
		||||
        choices=Connectivity.choices, default=Connectivity.UNKNOWN,
 | 
			
		||||
        max_length=16, verbose_name=_('Connectivity')
 | 
			
		||||
    )
 | 
			
		||||
    date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ class BaseAccount(JMSOrgBaseModel):
 | 
			
		|||
    name = models.CharField(max_length=128, verbose_name=_("Name"))
 | 
			
		||||
    username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
 | 
			
		||||
    secret_type = models.CharField(
 | 
			
		||||
        max_length=16, choices=SecretType.choices, default=SecretType.password, verbose_name=_('Secret type')
 | 
			
		||||
        max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
 | 
			
		||||
    )
 | 
			
		||||
    secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
 | 
			
		||||
    privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
 | 
			
		||||
| 
						 | 
				
			
			@ -65,25 +65,25 @@ class BaseAccount(JMSOrgBaseModel):
 | 
			
		|||
    @property
 | 
			
		||||
    def specific(self):
 | 
			
		||||
        data = {}
 | 
			
		||||
        if self.secret_type != SecretType.ssh_key:
 | 
			
		||||
        if self.secret_type != SecretType.SSH_KEY:
 | 
			
		||||
            return data
 | 
			
		||||
        data['ssh_key_fingerprint'] = self.ssh_key_fingerprint
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def private_key(self):
 | 
			
		||||
        if self.secret_type == SecretType.ssh_key:
 | 
			
		||||
        if self.secret_type == SecretType.SSH_KEY:
 | 
			
		||||
            return self.secret
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @private_key.setter
 | 
			
		||||
    def private_key(self, value):
 | 
			
		||||
        self.secret = value
 | 
			
		||||
        self.secret_type = SecretType.ssh_key
 | 
			
		||||
        self.secret_type = SecretType.SSH_KEY
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def public_key(self):
 | 
			
		||||
        if self.secret_type == SecretType.ssh_key:
 | 
			
		||||
        if self.secret_type == SecretType.SSH_KEY:
 | 
			
		||||
            return ssh_pubkey_gen(private_key=self.private_key)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +113,7 @@ class BaseAccount(JMSOrgBaseModel):
 | 
			
		|||
 | 
			
		||||
    @property
 | 
			
		||||
    def private_key_path(self):
 | 
			
		||||
        if not self.secret_type != SecretType.ssh_key or not self.secret:
 | 
			
		||||
        if not self.secret_type != SecretType.SSH_KEY or not self.secret:
 | 
			
		||||
            return None
 | 
			
		||||
        project_dir = settings.PROJECT_DIR
 | 
			
		||||
        tmp_dir = os.path.join(project_dir, 'tmp')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -201,7 +201,7 @@ class CommandFilterRule(OrgModelMixin):
 | 
			
		|||
            q |= Q(user_groups__in=set(user_groups))
 | 
			
		||||
        if account:
 | 
			
		||||
            org_id = account.org_id
 | 
			
		||||
            q |= Q(accounts__contains=list(account)) |\
 | 
			
		||||
            q |= Q(accounts__contains=account.username) | \
 | 
			
		||||
                 Q(accounts__contains=SpecialAccount.ALL.value)
 | 
			
		||||
        if asset:
 | 
			
		||||
            org_id = asset.org_id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ from common.db.fields import JsonDictTextField
 | 
			
		|||
 | 
			
		||||
from assets.const import Protocol
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,11 +48,15 @@ class PlatformAutomation(models.Model):
 | 
			
		|||
    push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
 | 
			
		||||
    push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method"))
 | 
			
		||||
    change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
 | 
			
		||||
    change_secret_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
 | 
			
		||||
    change_secret_method = models.TextField(
 | 
			
		||||
        max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
 | 
			
		||||
    verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
 | 
			
		||||
    verify_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
 | 
			
		||||
    verify_account_method = models.TextField(
 | 
			
		||||
        max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
 | 
			
		||||
    gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
 | 
			
		||||
    gather_accounts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
 | 
			
		||||
    gather_accounts_method = models.TextField(
 | 
			
		||||
        max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Platform(models.Model):
 | 
			
		||||
| 
						 | 
				
			
			@ -61,10 +64,11 @@ class Platform(models.Model):
 | 
			
		|||
    对资产提供 约束和默认值
 | 
			
		||||
    对资产进行抽象
 | 
			
		||||
    """
 | 
			
		||||
    CHARSET_CHOICES = (
 | 
			
		||||
        ('utf8', 'UTF-8'),
 | 
			
		||||
        ('gbk', 'GBK'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class CharsetChoices(models.TextChoices):
 | 
			
		||||
        utf8 = 'utf8', 'UTF-8'
 | 
			
		||||
        gbk = 'gbk', 'GBK'
 | 
			
		||||
 | 
			
		||||
    name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
 | 
			
		||||
    category = models.CharField(default='host', max_length=32, verbose_name=_("Category"))
 | 
			
		||||
    type = models.CharField(max_length=32, default='linux', verbose_name=_("Type"))
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +76,9 @@ class Platform(models.Model):
 | 
			
		|||
    internal = models.BooleanField(default=False, verbose_name=_("Internal"))
 | 
			
		||||
    comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
 | 
			
		||||
    # 资产有关的
 | 
			
		||||
    charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset"))
 | 
			
		||||
    charset = models.CharField(
 | 
			
		||||
        default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
 | 
			
		||||
    )
 | 
			
		||||
    domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
 | 
			
		||||
    protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
 | 
			
		||||
    # 账号有关的
 | 
			
		||||
| 
						 | 
				
			
			@ -103,4 +109,3 @@ class Platform(models.Model):
 | 
			
		|||
    class Meta:
 | 
			
		||||
        verbose_name = _("Platform")
 | 
			
		||||
        # ordering = ('name',)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,4 +11,4 @@ from .account import *
 | 
			
		|||
from assets.serializers.account.backup import *
 | 
			
		||||
from .platform import *
 | 
			
		||||
from .cagegory import *
 | 
			
		||||
from .automation import *
 | 
			
		||||
from .automations import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,11 @@ from django.utils.translation import ugettext_lazy as _
 | 
			
		|||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.drf.serializers import SecretReadableMixin
 | 
			
		||||
from common.drf.fields import ObjectRelatedField
 | 
			
		||||
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
 | 
			
		||||
from assets.tasks import push_accounts_to_assets
 | 
			
		||||
from assets.models import Account, AccountTemplate, Asset
 | 
			
		||||
from .base import BaseAccountSerializer
 | 
			
		||||
from assets.const import SecretType
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccountSerializerCreateMixin(serializers.ModelSerializer):
 | 
			
		||||
| 
						 | 
				
			
			@ -91,6 +92,8 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class AccountHistorySerializer(serializers.ModelSerializer):
 | 
			
		||||
    secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Account.history.model
 | 
			
		||||
        fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ from rest_framework import serializers
 | 
			
		|||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
from ops.mixin import PeriodTaskSerializerMixin
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from common.const.choices import Trigger
 | 
			
		||||
from common.drf.fields import LabeledChoiceField
 | 
			
		||||
 | 
			
		||||
from assets.models import AccountBackupPlan, AccountBackupPlanExecution
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +22,7 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
 | 
			
		|||
        fields = [
 | 
			
		||||
            'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created',
 | 
			
		||||
            'date_updated', 'created_by', 'periodic_display', 'comment',
 | 
			
		||||
            'recipients', 'categories'
 | 
			
		||||
            'recipients', 'types'
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'name': {'required': True},
 | 
			
		||||
| 
						 | 
				
			
			@ -32,17 +34,12 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    trigger_display = serializers.ReadOnlyField(
 | 
			
		||||
        source='get_trigger_display', label=_('Trigger mode')
 | 
			
		||||
    )
 | 
			
		||||
    trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AccountBackupPlanExecution
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
 | 
			
		||||
            'is_success', 'plan', 'org_id', 'recipients', 'trigger_display'
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = (
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
 | 
			
		||||
            'is_success', 'org_id', 'recipients'
 | 
			
		||||
        )
 | 
			
		||||
        ]
 | 
			
		||||
        fields = read_only_fields + ['plan']
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +1,21 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
from io import StringIO
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.utils import validate_ssh_private_key, ssh_private_key_gen
 | 
			
		||||
from common.drf.fields import EncryptedField
 | 
			
		||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
from assets.models import BaseAccount
 | 
			
		||||
from assets.serializers.base import AuthValidateMixin
 | 
			
		||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
 | 
			
		||||
__all__ = ['BaseAccountSerializer']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseAccountSerializer(BulkOrgResourceModelSerializer):
 | 
			
		||||
    secret = EncryptedField(
 | 
			
		||||
        label=_('Secret'), required=False, allow_blank=True,
 | 
			
		||||
        allow_null=True, max_length=40960
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = BaseAccount
 | 
			
		||||
        fields_mini = ['id', 'name', 'username']
 | 
			
		||||
        fields_small = fields_mini + ['privileged', 'secret_type', 'secret', 'has_secret', 'specific']
 | 
			
		||||
        fields_small = fields_mini + [
 | 
			
		||||
            'secret_type', 'secret', 'has_secret', 'passphrase',
 | 
			
		||||
            'privileged', 'is_active', 'specific',
 | 
			
		||||
        ]
 | 
			
		||||
        fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
 | 
			
		||||
        fields = fields_small + fields_other
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
| 
						 | 
				
			
			@ -29,28 +23,5 @@ class BaseAccountSerializer(BulkOrgResourceModelSerializer):
 | 
			
		|||
            'date_verified', 'created_by', 'date_created',
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'secret': {'write_only': True},
 | 
			
		||||
            'passphrase': {'write_only': True},
 | 
			
		||||
            'specific': {'label': _('Specific')},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def validate_private_key(self, private_key):
 | 
			
		||||
        if not private_key:
 | 
			
		||||
            return ''
 | 
			
		||||
        passphrase = self.initial_data.get('passphrase')
 | 
			
		||||
        passphrase = passphrase if passphrase else None
 | 
			
		||||
        valid = validate_ssh_private_key(private_key, password=passphrase)
 | 
			
		||||
        if not valid:
 | 
			
		||||
            raise serializers.ValidationError(_("private key invalid or passphrase error"))
 | 
			
		||||
 | 
			
		||||
        private_key = ssh_private_key_gen(private_key, password=passphrase)
 | 
			
		||||
        string_io = StringIO()
 | 
			
		||||
        private_key.write_private_key(string_io)
 | 
			
		||||
        private_key = string_io.getvalue()
 | 
			
		||||
        return private_key
 | 
			
		||||
 | 
			
		||||
    def validate_secret(self, value):
 | 
			
		||||
        secret_type = self.initial_data.get('secret_type')
 | 
			
		||||
        if secret_type == 'ssh_key':
 | 
			
		||||
            value = self.validate_private_key(value)
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
 | 
			
		|||
            'nodes', 'labels', 'protocols', 'accounts', 'nodes_display',
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'category', 'type', 'specific',
 | 
			
		||||
            'category', 'type', 'specific', 'info',
 | 
			
		||||
            'connectivity', 'date_verified',
 | 
			
		||||
            'created_by', 'date_created',
 | 
			
		||||
        ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
from django.utils.translation import ugettext as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
 | 
			
		||||
from assets.models import ChangeSecretRecord
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__file__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
 | 
			
		||||
    asset = serializers.SerializerMethodField(label=_('Asset'))
 | 
			
		||||
    account = serializers.SerializerMethodField(label=_('Account'))
 | 
			
		||||
    is_success = serializers.SerializerMethodField(label=_('Is success'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ChangeSecretRecord
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'asset', 'account', 'old_secret', 'new_secret',
 | 
			
		||||
            'status', 'error', 'is_success'
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_asset(instance):
 | 
			
		||||
        return str(instance.asset)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_account(instance):
 | 
			
		||||
        return str(instance.account)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_is_success(obj):
 | 
			
		||||
        if obj.status == 'success':
 | 
			
		||||
            return _("Success")
 | 
			
		||||
        return _("Failed")
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
from .base import *
 | 
			
		||||
from .change_secret import *
 | 
			
		||||
from .gather_accounts import *
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
from django.utils.translation import ugettext as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from ops.mixin import PeriodTaskSerializerMixin
 | 
			
		||||
from assets.const import AutomationTypes
 | 
			
		||||
from assets.models import Asset, Node, BaseAutomation, AutomationExecution
 | 
			
		||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from common.drf.fields import ObjectRelatedField
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__file__)
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'BaseAutomationSerializer', 'AutomationExecutionSerializer',
 | 
			
		||||
    'UpdateAssetSerializer', 'UpdateNodeSerializer', 'AutomationAssetsSerializer',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
 | 
			
		||||
    assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
 | 
			
		||||
    nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'date_created', 'date_updated', 'created_by', 'periodic_display'
 | 
			
		||||
        ]
 | 
			
		||||
        fields = read_only_fields + [
 | 
			
		||||
            'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment',
 | 
			
		||||
            'type', 'accounts', 'nodes', 'assets', 'is_active'
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'name': {'required': True},
 | 
			
		||||
            'type': {'read_only': True},
 | 
			
		||||
            'periodic_display': {'label': _('Periodic perform')},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationExecutionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    snapshot = serializers.SerializerMethodField(label=_('Automation snapshot'))
 | 
			
		||||
    type = serializers.ChoiceField(choices=AutomationTypes.choices, write_only=True, label=_('Type'))
 | 
			
		||||
    trigger_display = serializers.ReadOnlyField(source='get_trigger_display', label=_('Trigger mode'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = AutomationExecution
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status'
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_snapshot(obj):
 | 
			
		||||
        tp = obj.snapshot['type']
 | 
			
		||||
        snapshot = {
 | 
			
		||||
            'type': tp,
 | 
			
		||||
            'name': obj.snapshot['name'],
 | 
			
		||||
            'comment': obj.snapshot['comment'],
 | 
			
		||||
            'accounts': obj.snapshot['accounts'],
 | 
			
		||||
            'node_amount': len(obj.snapshot['nodes']),
 | 
			
		||||
            'asset_amount': len(obj.snapshot['assets']),
 | 
			
		||||
            'type_display': getattr(AutomationTypes, tp).label,
 | 
			
		||||
        }
 | 
			
		||||
        return snapshot
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateAssetSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = BaseAutomation
 | 
			
		||||
        fields = ['id', 'assets']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateNodeSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = BaseAutomation
 | 
			
		||||
        fields = ['id', 'nodes']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AutomationAssetsSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Asset
 | 
			
		||||
        only_fields = ['id', 'name', 'address']
 | 
			
		||||
        fields = tuple(only_fields)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,133 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from common.drf.fields import LabeledChoiceField, ObjectRelatedField
 | 
			
		||||
from assets.serializers.base import AuthValidateMixin
 | 
			
		||||
from assets.const import DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy
 | 
			
		||||
from assets.models import Asset, Account, ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
 | 
			
		||||
 | 
			
		||||
from .base import BaseAutomationSerializer
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__file__)
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'ChangeSecretAutomationSerializer',
 | 
			
		||||
    'ChangeSecretRecordSerializer',
 | 
			
		||||
    'ChangeSecretRecordBackUpSerializer'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer):
 | 
			
		||||
    secret_strategy = LabeledChoiceField(
 | 
			
		||||
        choices=SecretStrategy.choices, required=True, label=_('Secret strategy')
 | 
			
		||||
    )
 | 
			
		||||
    ssh_key_change_strategy = LabeledChoiceField(
 | 
			
		||||
        choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
 | 
			
		||||
    )
 | 
			
		||||
    password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ChangeSecretAutomation
 | 
			
		||||
        read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
 | 
			
		||||
        fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
 | 
			
		||||
            'secret_type', 'secret_strategy', 'secret', 'password_rules',
 | 
			
		||||
            'ssh_key_change_strategy', 'passphrase', 'recipients',
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
 | 
			
		||||
            'recipients': {'label': _('Recipient'), 'help_text': _(
 | 
			
		||||
                "Currently only mail sending is supported"
 | 
			
		||||
            )},
 | 
			
		||||
        }}
 | 
			
		||||
 | 
			
		||||
    def validate_password_rules(self, password_rules):
 | 
			
		||||
        secret_type = self.initial_secret_type
 | 
			
		||||
        if secret_type != SecretType.PASSWORD:
 | 
			
		||||
            return password_rules
 | 
			
		||||
 | 
			
		||||
        length = password_rules.get('length')
 | 
			
		||||
        symbol_set = password_rules.get('symbol_set', '')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            length = int(length)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(e)
 | 
			
		||||
            msg = _("* Please enter the correct password length")
 | 
			
		||||
            raise serializers.ValidationError(msg)
 | 
			
		||||
        if length < 6 or length > 30:
 | 
			
		||||
            msg = _('* Password length range 6-30 bits')
 | 
			
		||||
            raise serializers.ValidationError(msg)
 | 
			
		||||
 | 
			
		||||
        if not isinstance(symbol_set, str):
 | 
			
		||||
            symbol_set = str(symbol_set)
 | 
			
		||||
 | 
			
		||||
        password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
 | 
			
		||||
        return password_rules
 | 
			
		||||
 | 
			
		||||
    def validate(self, attrs):
 | 
			
		||||
        secret_type = attrs.get('secret_type')
 | 
			
		||||
        secret_strategy = attrs.get('secret_strategy')
 | 
			
		||||
        if secret_type == SecretType.PASSWORD:
 | 
			
		||||
            attrs.pop('ssh_key_change_strategy', None)
 | 
			
		||||
            if secret_strategy == SecretStrategy.custom:
 | 
			
		||||
                attrs.pop('password_rules', None)
 | 
			
		||||
            else:
 | 
			
		||||
                attrs.pop('secret', None)
 | 
			
		||||
        elif secret_type == SecretType.SSH_KEY:
 | 
			
		||||
            attrs.pop('password_rules', None)
 | 
			
		||||
            if secret_strategy != SecretStrategy.custom:
 | 
			
		||||
                attrs.pop('secret', None)
 | 
			
		||||
        return attrs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangeSecretRecordSerializer(serializers.ModelSerializer):
 | 
			
		||||
    is_success = serializers.SerializerMethodField(label=_('Is success'))
 | 
			
		||||
    asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
 | 
			
		||||
    account = ObjectRelatedField(queryset=Account.objects, label=_('Account'))
 | 
			
		||||
    execution = ObjectRelatedField(
 | 
			
		||||
        queryset=AutomationExecution.objects, label=_('Automation task execution')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ChangeSecretRecord
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'asset', 'account', 'date_started',
 | 
			
		||||
            'date_finished', 'is_success', 'error', 'execution',
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = fields
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_is_success(obj):
 | 
			
		||||
        if obj.status == 'success':
 | 
			
		||||
            return _("Success")
 | 
			
		||||
        return _("Failed")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
 | 
			
		||||
    asset = serializers.SerializerMethodField(label=_('Asset'))
 | 
			
		||||
    account = serializers.SerializerMethodField(label=_('Account'))
 | 
			
		||||
    is_success = serializers.SerializerMethodField(label=_('Is success'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ChangeSecretRecord
 | 
			
		||||
        fields = [
 | 
			
		||||
            'id', 'asset', 'account', 'old_secret', 'new_secret',
 | 
			
		||||
            'status', 'error', 'is_success'
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = fields
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_asset(instance):
 | 
			
		||||
        return str(instance.asset)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_account(instance):
 | 
			
		||||
        return str(instance.account)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_is_success(obj):
 | 
			
		||||
        if obj.status == 'success':
 | 
			
		||||
            return _("Success")
 | 
			
		||||
        return _("Failed")
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from assets.models import GatherAccountsAutomation
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
 | 
			
		||||
from .base import BaseAutomationSerializer
 | 
			
		||||
 | 
			
		||||
logger = get_logger(__file__)
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'GatherAccountAutomationSerializer',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GatherAccountAutomationSerializer(BaseAutomationSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = GatherAccountsAutomation
 | 
			
		||||
        read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + ['executed_amount']
 | 
			
		||||
        fields = BaseAutomationSerializer.Meta.fields + read_only_fields
 | 
			
		||||
 | 
			
		||||
        extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
 | 
			
		||||
            'executed_amount': {'label': _('Executed amount')}
 | 
			
		||||
        }}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,68 +1,53 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
from io import StringIO
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
 | 
			
		||||
from common.drf.fields import EncryptedField
 | 
			
		||||
from .utils import validate_password_for_ansible
 | 
			
		||||
from assets.const import SecretType
 | 
			
		||||
from common.drf.fields import EncryptedField, LabeledChoiceField
 | 
			
		||||
from .utils import validate_password_for_ansible, validate_ssh_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthValidateMixin(serializers.Serializer):
 | 
			
		||||
    password = EncryptedField(
 | 
			
		||||
        label=_('Password'), required=False, allow_blank=True, allow_null=True,
 | 
			
		||||
        max_length=1024, validators=[validate_password_for_ansible]
 | 
			
		||||
    secret_type = LabeledChoiceField(
 | 
			
		||||
        choices=SecretType.choices, required=True, label=_('Secret type')
 | 
			
		||||
    )
 | 
			
		||||
    private_key = EncryptedField(
 | 
			
		||||
        label=_('SSH private key'), required=False, allow_blank=True,
 | 
			
		||||
        allow_null=True, max_length=16384
 | 
			
		||||
    secret = EncryptedField(
 | 
			
		||||
        label=_('Secret'), required=False, max_length=40960, allow_blank=True,
 | 
			
		||||
        allow_null=True, write_only=True,
 | 
			
		||||
    )
 | 
			
		||||
    passphrase = serializers.CharField(
 | 
			
		||||
        allow_blank=True, allow_null=True, required=False, max_length=512,
 | 
			
		||||
        write_only=True, label=_('Key password')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate_private_key(self, private_key):
 | 
			
		||||
        if not private_key:
 | 
			
		||||
            return
 | 
			
		||||
        passphrase = self.initial_data.get('passphrase')
 | 
			
		||||
        passphrase = passphrase if passphrase else None
 | 
			
		||||
        valid = validate_ssh_private_key(private_key, password=passphrase)
 | 
			
		||||
        if not valid:
 | 
			
		||||
            raise serializers.ValidationError(_("private key invalid or passphrase error"))
 | 
			
		||||
    @property
 | 
			
		||||
    def initial_secret_type(self):
 | 
			
		||||
        secret_type = self.initial_data.get('secret_type')
 | 
			
		||||
        return secret_type
 | 
			
		||||
 | 
			
		||||
        private_key = ssh_private_key_gen(private_key, password=passphrase)
 | 
			
		||||
        string_io = StringIO()
 | 
			
		||||
        private_key.write_private_key(string_io)
 | 
			
		||||
        private_key = string_io.getvalue()
 | 
			
		||||
        return private_key
 | 
			
		||||
    def validate_secret(self, secret):
 | 
			
		||||
        if not secret:
 | 
			
		||||
            return
 | 
			
		||||
        secret_type = self.initial_secret_type
 | 
			
		||||
        if secret_type == SecretType.PASSWORD:
 | 
			
		||||
            validate_password_for_ansible(secret)
 | 
			
		||||
            return secret
 | 
			
		||||
        elif secret_type == SecretType.SSH_KEY:
 | 
			
		||||
            passphrase = self.initial_data.get('passphrase')
 | 
			
		||||
            passphrase = passphrase if passphrase else None
 | 
			
		||||
            return validate_ssh_key(secret, passphrase)
 | 
			
		||||
        else:
 | 
			
		||||
            return secret
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def clean_auth_fields(validated_data):
 | 
			
		||||
        for field in ('password', 'private_key', 'public_key'):
 | 
			
		||||
        for field in ('secret',):
 | 
			
		||||
            value = validated_data.get(field)
 | 
			
		||||
            if not value:
 | 
			
		||||
                validated_data.pop(field, None)
 | 
			
		||||
        validated_data.pop('passphrase', None)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _validate_gen_key(attrs):
 | 
			
		||||
        private_key = attrs.get('private_key')
 | 
			
		||||
        if not private_key:
 | 
			
		||||
            return attrs
 | 
			
		||||
 | 
			
		||||
        password = attrs.get('passphrase')
 | 
			
		||||
        username = attrs.get('username')
 | 
			
		||||
        public_key = ssh_pubkey_gen(private_key, password=password, username=username)
 | 
			
		||||
        attrs['public_key'] = public_key
 | 
			
		||||
        return attrs
 | 
			
		||||
 | 
			
		||||
    def validate(self, attrs):
 | 
			
		||||
        attrs = self._validate_gen_key(attrs)
 | 
			
		||||
        return super().validate(attrs)
 | 
			
		||||
 | 
			
		||||
    def create(self, validated_data):
 | 
			
		||||
        self.clean_auth_fields(validated_data)
 | 
			
		||||
        return super().create(validated_data)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,15 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
 | 
			
		||||
from ..models import GatheredUser
 | 
			
		||||
from common.drf.fields import ObjectRelatedField
 | 
			
		||||
from ..models import GatheredUser, Asset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
 | 
			
		||||
    asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = GatheredUser
 | 
			
		||||
        fields_mini = ['id']
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,10 @@
 | 
			
		|||
from io import StringIO
 | 
			
		||||
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.utils import ssh_private_key_gen, validate_ssh_private_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_password_for_ansible(password):
 | 
			
		||||
    """ 校验 Ansible 不支持的特殊字符 """
 | 
			
		||||
| 
						 | 
				
			
			@ -15,3 +19,14 @@ def validate_password_for_ansible(password):
 | 
			
		|||
    if '"' in password:
 | 
			
		||||
        raise serializers.ValidationError(_('Password can not contains `"` '))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_ssh_key(ssh_key, passphrase=None):
 | 
			
		||||
    valid = validate_ssh_private_key(ssh_key, password=passphrase)
 | 
			
		||||
    if not valid:
 | 
			
		||||
        raise serializers.ValidationError(_("private key invalid or passphrase error"))
 | 
			
		||||
 | 
			
		||||
    ssh_key = ssh_private_key_gen(ssh_key, password=passphrase)
 | 
			
		||||
    string_io = StringIO()
 | 
			
		||||
    ssh_key.write_private_key(string_io)
 | 
			
		||||
    ssh_key = string_io.getvalue()
 | 
			
		||||
    return ssh_key
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,3 +9,4 @@ from .gather_facts import *
 | 
			
		|||
from .nodes_amount import *
 | 
			
		||||
from .push_account import *
 | 
			
		||||
from .verify_account import *
 | 
			
		||||
from .gather_accounts import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,9 +7,9 @@ logger = get_logger(__file__)
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
@shared_task(queue='ansible')
 | 
			
		||||
def execute_automation(pid, trigger, mode):
 | 
			
		||||
def execute_automation(pid, trigger, model):
 | 
			
		||||
    with tmp_to_root_org():
 | 
			
		||||
        instance = get_object_or_none(mode, pk=pid)
 | 
			
		||||
        instance = get_object_or_none(model, pk=pid)
 | 
			
		||||
    if not instance:
 | 
			
		||||
        logger.error("No automation task found: {}".format(pid))
 | 
			
		||||
        return
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
# ~*~ coding: utf-8 ~*~
 | 
			
		||||
from celery import shared_task
 | 
			
		||||
from django.utils.translation import gettext_noop
 | 
			
		||||
 | 
			
		||||
from orgs.utils import tmp_to_root_org, org_aware_func
 | 
			
		||||
from common.utils import get_logger
 | 
			
		||||
from assets.models import Node
 | 
			
		||||
 | 
			
		||||
__all__ = ['gather_asset_accounts']
 | 
			
		||||
logger = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@org_aware_func("nodes")
 | 
			
		||||
def gather_asset_accounts_util(nodes, task_name):
 | 
			
		||||
    from assets.models import GatherAccountsAutomation
 | 
			
		||||
    task_name = GatherAccountsAutomation.generate_unique_name(task_name)
 | 
			
		||||
 | 
			
		||||
    data = {
 | 
			
		||||
        'name': task_name,
 | 
			
		||||
        'comment': ', '.join([str(i) for i in nodes])
 | 
			
		||||
    }
 | 
			
		||||
    instance = GatherAccountsAutomation.objects.create(**data)
 | 
			
		||||
    instance.nodes.add(*nodes)
 | 
			
		||||
    instance.execute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shared_task(queue="ansible")
 | 
			
		||||
def gather_asset_accounts(node_ids, task_name=None):
 | 
			
		||||
    if task_name is None:
 | 
			
		||||
        task_name = gettext_noop("Gather assets accounts")
 | 
			
		||||
 | 
			
		||||
    with tmp_to_root_org():
 | 
			
		||||
        nodes = Node.objects.filter(id__in=node_ids)
 | 
			
		||||
    gather_asset_accounts_util(nodes=nodes, task_name=task_name)
 | 
			
		||||
| 
						 | 
				
			
			@ -27,17 +27,26 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
 | 
			
		|||
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
 | 
			
		||||
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
 | 
			
		||||
 | 
			
		||||
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
 | 
			
		||||
router.register(r'automation-executions', api.AutomationExecutionViewSet, 'automation-execution')
 | 
			
		||||
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
 | 
			
		||||
router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation')
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    # path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
 | 
			
		||||
    path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(), name='asset-perm-user-permission-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(),
 | 
			
		||||
         name='asset-perm-user-permission-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(),
 | 
			
		||||
         name='asset-perm-user-group-list'),
 | 
			
		||||
    path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/',
 | 
			
		||||
         api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'),
 | 
			
		||||
 | 
			
		||||
    path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'),
 | 
			
		||||
    path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(), name='account-secret-history'),
 | 
			
		||||
    path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
 | 
			
		||||
         name='account-secret-history'),
 | 
			
		||||
 | 
			
		||||
    path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'),
 | 
			
		||||
    path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
 | 
			
		||||
| 
						 | 
				
			
			@ -52,7 +61,11 @@ urlpatterns = [
 | 
			
		|||
    path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
 | 
			
		||||
 | 
			
		||||
    path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
 | 
			
		||||
 | 
			
		||||
    path('automation/<uuid:pk>/asset/remove/', api.AutomationRemoveAssetApi.as_view(), name='automation-remove-asset'),
 | 
			
		||||
    path('automation/<uuid:pk>/asset/add/', api.AutomationAddAssetApi.as_view(), name='automation-add-asset'),
 | 
			
		||||
    path('automation/<uuid:pk>/nodes/', api.AutomationNodeAddRemoveApi.as_view(), name='automation-add-or-remove-node'),
 | 
			
		||||
    path('automation/<uuid:pk>/assets/', api.AutomationAssetsListApi.as_view(), name='automation-assets'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
urlpatterns += router.urls
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -178,8 +178,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
 | 
			
		|||
    get_object: callable
 | 
			
		||||
    get_serializer: callable
 | 
			
		||||
    perform_create: callable
 | 
			
		||||
    check_token_permission: callable
 | 
			
		||||
    create_connection_token: callable
 | 
			
		||||
 | 
			
		||||
    @action(methods=['POST'], detail=False, url_path='secret-info/detail')
 | 
			
		||||
    def get_secret_detail(self, request, *args, **kwargs):
 | 
			
		||||
| 
						 | 
				
			
			@ -277,10 +275,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
 | 
			
		|||
        from perms.utils.account import PermAccountUtil
 | 
			
		||||
        actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
 | 
			
		||||
        if not actions:
 | 
			
		||||
            error = ''
 | 
			
		||||
            error = 'No actions'
 | 
			
		||||
            raise PermissionDenied(error)
 | 
			
		||||
        if expire_at < time.time():
 | 
			
		||||
            error = ''
 | 
			
		||||
            error = 'Expired'
 | 
			
		||||
            raise PermissionDenied(error)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import base64
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import redirect, reverse, render
 | 
			
		||||
from django.utils.deprecation import MiddlewareMixin
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
 | 
			
		|||
            is_valid = False
 | 
			
		||||
            error = _('No user or invalid user')
 | 
			
		||||
            return is_valid, error
 | 
			
		||||
        if not self.asset or self.asset.is_active:
 | 
			
		||||
        if not self.asset or not self.asset.is_active:
 | 
			
		||||
            is_valid = False
 | 
			
		||||
            error = _('No asset or inactive asset')
 | 
			
		||||
            return is_valid, error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -159,7 +159,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
 | 
			
		|||
    domain = ConnectionTokenDomainSerializer(read_only=True)
 | 
			
		||||
    cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
 | 
			
		||||
    actions = ActionsField()
 | 
			
		||||
    expired_at = serializers.IntegerField()
 | 
			
		||||
    expire_at = serializers.IntegerField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ConnectionToken
 | 
			
		||||
| 
						 | 
				
			
			@ -167,5 +167,5 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
 | 
			
		|||
            'id', 'secret',
 | 
			
		||||
            'user', 'asset', 'account_username', 'account', 'protocol',
 | 
			
		||||
            'domain', 'gateway', 'cmd_filter_rules',
 | 
			
		||||
            'actions', 'expired_at',
 | 
			
		||||
            'actions', 'expire_at',
 | 
			
		||||
        ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,9 @@ class BaseService(object):
 | 
			
		|||
        if self.is_running:
 | 
			
		||||
            msg = f'{self.name} is running: {self.pid}.'
 | 
			
		||||
        else:
 | 
			
		||||
            msg = f'{self.name} is stopped.'
 | 
			
		||||
            msg = '\033[31m{} is stopped.\033[0m\nYou can manual start it to find the error: \n' \
 | 
			
		||||
                  '  $ cd {}\n' \
 | 
			
		||||
                  '  $ {}'.format(self.name, self.cwd, ' '.join(self.cmd))
 | 
			
		||||
        print(msg)
 | 
			
		||||
 | 
			
		||||
    # -- log --
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,6 @@ class ServicesUtil(object):
 | 
			
		|||
    def clean_up(self):
 | 
			
		||||
        if not self.EXIT_EVENT.is_set():
 | 
			
		||||
            self.EXIT_EVENT.set()
 | 
			
		||||
 | 
			
		||||
        self.stop()
 | 
			
		||||
 | 
			
		||||
    def show_status(self):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,14 +16,13 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
 | 
			
		|||
    """Allows access to valid user, is active and not expired"""
 | 
			
		||||
 | 
			
		||||
    def has_permission(self, request, view):
 | 
			
		||||
        return super(IsValidUser, self).has_permission(request, view) \
 | 
			
		||||
        return super().has_permission(request, view) \
 | 
			
		||||
               and request.user.is_valid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IsValidUserOrConnectionToken(IsValidUser):
 | 
			
		||||
 | 
			
		||||
    def has_permission(self, request, view):
 | 
			
		||||
        return super(IsValidUserOrConnectionToken, self).has_permission(request, view) \
 | 
			
		||||
        return super().has_permission(request, view) \
 | 
			
		||||
               or self.is_valid_connection_token(request)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +41,12 @@ class OnlySuperUser(IsValidUser):
 | 
			
		|||
               and request.user.is_superuser
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IsServiceAccount(IsValidUser):
 | 
			
		||||
    def has_permission(self, request, view):
 | 
			
		||||
        return super().has_permission(request, view) \
 | 
			
		||||
               and request.user.is_service_account
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WithBootstrapToken(permissions.BasePermission):
 | 
			
		||||
    def has_permission(self, request, view):
 | 
			
		||||
        authorization = request.META.get('HTTP_AUTHORIZATION', '')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -308,14 +308,14 @@ class HealthCheckView(HealthApiMixin):
 | 
			
		|||
    def get_db_status():
 | 
			
		||||
        t1 = time.time()
 | 
			
		||||
        try:
 | 
			
		||||
            User.objects.first()
 | 
			
		||||
            ok = User.objects.first() is not None
 | 
			
		||||
            t2 = time.time()
 | 
			
		||||
            return True, t2 - t1
 | 
			
		||||
        except:
 | 
			
		||||
            t2 = time.time()
 | 
			
		||||
            return False, t2 - t1
 | 
			
		||||
            return ok, t2 - t1
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return False, str(e)
 | 
			
		||||
 | 
			
		||||
    def get_redis_status(self):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_redis_status():
 | 
			
		||||
        key = 'HEALTH_CHECK'
 | 
			
		||||
 | 
			
		||||
        t1 = time.time()
 | 
			
		||||
| 
						 | 
				
			
			@ -324,12 +324,12 @@ class HealthCheckView(HealthApiMixin):
 | 
			
		|||
            cache.set(key, '1', 10)
 | 
			
		||||
            got = cache.get(key)
 | 
			
		||||
            t2 = time.time()
 | 
			
		||||
 | 
			
		||||
            if value == got:
 | 
			
		||||
                return True, t2 -t1
 | 
			
		||||
            return False, t2 -t1
 | 
			
		||||
        except:
 | 
			
		||||
            t2 = time.time()
 | 
			
		||||
            return False, t2 - t1
 | 
			
		||||
                return True, t2 - t1
 | 
			
		||||
            return False, 'Value not match'
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return False, str(e)
 | 
			
		||||
 | 
			
		||||
    def get(self, request):
 | 
			
		||||
        redis_status, redis_time = self.get_redis_status()
 | 
			
		||||
| 
						 | 
				
			
			@ -341,7 +341,7 @@ class HealthCheckView(HealthApiMixin):
 | 
			
		|||
            'db_time': db_time,
 | 
			
		||||
            'redis_status': redis_status,
 | 
			
		||||
            'redis_time': redis_time,
 | 
			
		||||
            'time': int(time.time())
 | 
			
		||||
            'time': int(time.time()),
 | 
			
		||||
        }
 | 
			
		||||
        return Response(data)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,9 @@
 | 
			
		|||
import os
 | 
			
		||||
import re
 | 
			
		||||
import pytz
 | 
			
		||||
import time
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.shortcuts import HttpResponse
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
| 
						 | 
				
			
			@ -92,3 +95,37 @@ class RefererCheckMiddleware:
 | 
			
		|||
            return HttpResponseForbidden('CSRF CHECK ERROR')
 | 
			
		||||
        response = self.get_response(request)
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StartMiddleware:
 | 
			
		||||
    def __init__(self, get_response):
 | 
			
		||||
        self.get_response = get_response
 | 
			
		||||
        if not settings.DEBUG_DEV:
 | 
			
		||||
            raise MiddlewareNotUsed
 | 
			
		||||
 | 
			
		||||
    def __call__(self, request):
 | 
			
		||||
        request._s_time_start = time.time()
 | 
			
		||||
        response = self.get_response(request)
 | 
			
		||||
        request._s_time_end = time.time()
 | 
			
		||||
        if request.path == '/api/health/':
 | 
			
		||||
            data = response.data
 | 
			
		||||
            data['pre_middleware_time'] = request._e_time_start - request._s_time_start
 | 
			
		||||
            data['api_time'] = request._e_time_end - request._e_time_start
 | 
			
		||||
            data['post_middleware_time'] = request._s_time_end - request._e_time_end
 | 
			
		||||
            response.content = json.dumps(data)
 | 
			
		||||
            response.headers['Content-Length'] = str(len(response.content))
 | 
			
		||||
            return response
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EndMiddleware:
 | 
			
		||||
    def __init__(self, get_response):
 | 
			
		||||
        self.get_response = get_response
 | 
			
		||||
        if not settings.DEBUG_DEV:
 | 
			
		||||
            raise MiddlewareNotUsed
 | 
			
		||||
 | 
			
		||||
    def __call__(self, request):
 | 
			
		||||
        request._e_time_start = time.time()
 | 
			
		||||
        response = self.get_response(request)
 | 
			
		||||
        request._e_time_end = time.time()
 | 
			
		||||
        return response
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,6 +86,7 @@ INSTALLED_APPS = [
 | 
			
		|||
]
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
    'jumpserver.middleware.StartMiddleware',
 | 
			
		||||
    'django.middleware.security.SecurityMiddleware',
 | 
			
		||||
    'django.contrib.sessions.middleware.SessionMiddleware',
 | 
			
		||||
    'django.middleware.locale.LocaleMiddleware',
 | 
			
		||||
| 
						 | 
				
			
			@ -105,6 +106,7 @@ MIDDLEWARE = [
 | 
			
		|||
    'authentication.middleware.ThirdPartyLoginMiddleware',
 | 
			
		||||
    'authentication.middleware.SessionCookieMiddleware',
 | 
			
		||||
    'simple_history.middleware.HistoryRequestMiddleware',
 | 
			
		||||
    'jumpserver.middleware.EndMiddleware',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
ROOT_URLCONF = 'jumpserver.urls'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
version https://git-lfs.github.com/spec/v1
 | 
			
		||||
oid sha256:0b396cc9a485f6474d14ca30a1a7ba4f954b07754148b964efbb21519c55b280
 | 
			
		||||
size 102849
 | 
			
		||||
oid sha256:314c29cb8b10aaddbb030bf49af293be23f0153ff1f1c7562946879574ce6de8
 | 
			
		||||
size 102801
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -858,7 +858,7 @@ msgstr "校验日期"
 | 
			
		|||
 | 
			
		||||
#: assets/models/base.py:63
 | 
			
		||||
msgid "Privileged"
 | 
			
		||||
msgstr "特权的"
 | 
			
		||||
msgstr "特权账号"
 | 
			
		||||
 | 
			
		||||
#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61
 | 
			
		||||
#: users/models/group.py:31 users/models/user.py:671
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,7 +144,7 @@ def check_server_performance_period():
 | 
			
		|||
    ServerPerformanceCheckUtil().check_and_publish()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shared_task(queue="ansible", verbose_name=_("Hello"), comment="an test shared task")
 | 
			
		||||
@shared_task(verbose_name=_("Hello"), comment="an test shared task")
 | 
			
		||||
def hello(name, callback=None):
 | 
			
		||||
    from users.models import User
 | 
			
		||||
    import time
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,16 @@
 | 
			
		|||
import uuid
 | 
			
		||||
import logging
 | 
			
		||||
from functools import reduce
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import F, Q, TextChoices
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
 | 
			
		||||
from common.utils import lazyproperty, date_expired_default
 | 
			
		||||
from common.db.models import BaseCreateUpdateModel, UnionQuerySet
 | 
			
		||||
from assets.models import Asset, Node, FamilyMixin, Account
 | 
			
		||||
from orgs.mixins.models import OrgModelMixin
 | 
			
		||||
from orgs.mixins.models import OrgManager
 | 
			
		||||
from common.utils import lazyproperty, date_expired_default
 | 
			
		||||
from common.db.models import BaseCreateUpdateModel, UnionQuerySet
 | 
			
		||||
from .const import Action, SpecialAccount
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,10 +6,11 @@ from rest_framework.fields import empty
 | 
			
		|||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from common.drf.fields import ObjectRelatedField
 | 
			
		||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
from assets.models import Asset, Node
 | 
			
		||||
from users.models import User, UserGroup
 | 
			
		||||
from perms.models import AssetPermission, Action
 | 
			
		||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
 | 
			
		||||
 | 
			
		||||
__all__ = ['AssetPermissionSerializer', 'ActionsField']
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,18 +45,10 @@ class ActionsDisplayField(ActionsField):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
 | 
			
		||||
    users_display = serializers.ListField(
 | 
			
		||||
        child=serializers.CharField(), label=_('Users display'), required=False
 | 
			
		||||
    )
 | 
			
		||||
    user_groups_display = serializers.ListField(
 | 
			
		||||
        child=serializers.CharField(), label=_('User groups display'), required=False
 | 
			
		||||
    )
 | 
			
		||||
    assets_display = serializers.ListField(
 | 
			
		||||
        child=serializers.CharField(), label=_('Assets display'), required=False
 | 
			
		||||
    )
 | 
			
		||||
    nodes_display = serializers.ListField(
 | 
			
		||||
        child=serializers.CharField(), label=_('Nodes display'), required=False
 | 
			
		||||
    )
 | 
			
		||||
    users = ObjectRelatedField(queryset=User.objects, many=True, required=False)
 | 
			
		||||
    user_groups = ObjectRelatedField(queryset=UserGroup.objects, many=True, required=False)
 | 
			
		||||
    assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False)
 | 
			
		||||
    nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False)
 | 
			
		||||
    actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
 | 
			
		||||
    is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
 | 
			
		||||
    is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
 | 
			
		||||
| 
						 | 
				
			
			@ -64,24 +57,16 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
 | 
			
		|||
        model = AssetPermission
 | 
			
		||||
        fields_mini = ['id', 'name']
 | 
			
		||||
        fields_small = fields_mini + [
 | 
			
		||||
            'is_active', 'is_expired', 'is_valid', 'actions',
 | 
			
		||||
            'accounts',
 | 
			
		||||
            'created_by', 'date_created', 'date_expired',
 | 
			
		||||
            'accounts', 'is_active', 'is_expired', 'is_valid',
 | 
			
		||||
            'actions', 'created_by', 'date_created', 'date_expired',
 | 
			
		||||
            'date_start', 'comment', 'from_ticket'
 | 
			
		||||
        ]
 | 
			
		||||
        fields_m2m = [
 | 
			
		||||
            'users', 'users_display', 'user_groups', 'user_groups_display', 'assets',
 | 
			
		||||
            'assets_display', 'nodes', 'nodes_display',
 | 
			
		||||
            'users_amount', 'user_groups_amount', 'assets_amount',
 | 
			
		||||
            'nodes_amount',
 | 
			
		||||
            'users', 'user_groups',  'assets', 'nodes',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = fields_small + fields_m2m
 | 
			
		||||
        read_only_fields = ['created_by', 'date_created', 'from_ticket']
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'users_amount': {'label': _('Users amount')},
 | 
			
		||||
            'user_groups_amount': {'label': _('User groups amount')},
 | 
			
		||||
            'assets_amount': {'label': _('Assets amount')},
 | 
			
		||||
            'nodes_amount': {'label': _('Nodes amount')},
 | 
			
		||||
            'actions': {'label': _('Actions')},
 | 
			
		||||
            'is_expired': {'label': _('Is expired')},
 | 
			
		||||
            'is_valid': {'label': _('Is valid')},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,9 @@ class PermAccountUtil(AssetPermissionUtil):
 | 
			
		|||
            user, asset, with_actions=True, with_perms=True
 | 
			
		||||
        )
 | 
			
		||||
        perm = perms.first()
 | 
			
		||||
        account = accounts.filter(username=account_username).first()
 | 
			
		||||
        actions = account.actions if account else []
 | 
			
		||||
        expire_at = perm.date_expired if perm else time.time()
 | 
			
		||||
        actions = []
 | 
			
		||||
        for account in accounts:
 | 
			
		||||
            if account.username == account_username:
 | 
			
		||||
                actions = account.actions
 | 
			
		||||
        expire_at = perm.date_expired.timestamp() if perm else time.time()
 | 
			
		||||
        return actions, expire_at
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +1,3 @@
 | 
			
		|||
from .applet import *
 | 
			
		||||
from .host import *
 | 
			
		||||
from .relation import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,10 +2,14 @@ from rest_framework import viewsets
 | 
			
		|||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from common.permissions import IsServiceAccount
 | 
			
		||||
from common.drf.api import JMSModelViewSet
 | 
			
		||||
from orgs.utils import tmp_to_builtin_org
 | 
			
		||||
from terminal import serializers
 | 
			
		||||
from terminal.models import AppletHost, Applet, AppletHostDeployment
 | 
			
		||||
from terminal.serializers import (
 | 
			
		||||
    AppletHostSerializer, AppletHostDeploymentSerializer,
 | 
			
		||||
    AppletHostStartupSerializer
 | 
			
		||||
)
 | 
			
		||||
from terminal.models import AppletHost, AppletHostDeployment
 | 
			
		||||
from terminal.tasks import run_applet_host_deployment
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -13,37 +17,29 @@ __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class AppletHostViewSet(JMSModelViewSet):
 | 
			
		||||
    serializer_class = serializers.AppletHostSerializer
 | 
			
		||||
    serializer_class = AppletHostSerializer
 | 
			
		||||
    queryset = AppletHost.objects.all()
 | 
			
		||||
    rbac_perms = {
 | 
			
		||||
        'accounts': 'terminal.view_applethost',
 | 
			
		||||
        'reports': '*'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @action(methods=['post'], detail=True, serializer_class=serializers.AppletHostReportSerializer)
 | 
			
		||||
    def reports(self, request, *args, **kwargs):
 | 
			
		||||
        # 1. Host 和 Terminal 关联
 | 
			
		||||
        # 2. 上报 安装的 Applets 每小时
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        with tmp_to_builtin_org(system=1):
 | 
			
		||||
            return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_permissions(self):
 | 
			
		||||
        if self.action == 'startup':
 | 
			
		||||
            return [IsServiceAccount()]
 | 
			
		||||
        return super().get_permissions()
 | 
			
		||||
 | 
			
		||||
    @action(methods=['post'], detail=True, serializer_class=AppletHostStartupSerializer)
 | 
			
		||||
    def startup(self, request, *args, **kwargs):
 | 
			
		||||
        instance = self.get_object()
 | 
			
		||||
        serializer = self.get_serializer(data=request.data)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
 | 
			
		||||
        data = serializer.validated_data
 | 
			
		||||
        instance.check_terminal_binding(request)
 | 
			
		||||
        instance.check_applets_state(data['applets'])
 | 
			
		||||
        return Response({'msg': 'ok'})
 | 
			
		||||
 | 
			
		||||
    @action(methods=['get'], detail=True, serializer_class=serializers.AppletHostAccountSerializer)
 | 
			
		||||
    def accounts(self, request, *args, **kwargs):
 | 
			
		||||
        host = self.get_object()
 | 
			
		||||
        with tmp_to_builtin_org(system=1):
 | 
			
		||||
            accounts = host.accounts.all().filter(privileged=False)
 | 
			
		||||
        response = self.get_paginated_response_from_queryset(accounts)
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
 | 
			
		||||
    serializer_class = serializers.AppletHostDeploymentSerializer
 | 
			
		||||
    serializer_class = AppletHostDeploymentSerializer
 | 
			
		||||
    queryset = AppletHostDeployment.objects.all()
 | 
			
		||||
 | 
			
		||||
    def create(self, request, *args, **kwargs):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,86 @@
 | 
			
		|||
from typing import Callable
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
 | 
			
		||||
from common.drf.api import JMSModelViewSet
 | 
			
		||||
from common.permissions import IsServiceAccount
 | 
			
		||||
from common.utils import is_uuid
 | 
			
		||||
from orgs.utils import tmp_to_builtin_org
 | 
			
		||||
from rbac.permissions import RBACPermission
 | 
			
		||||
from terminal.models import AppletHost
 | 
			
		||||
from terminal.serializers import (
 | 
			
		||||
    AppletHostAccountSerializer,
 | 
			
		||||
    AppletPublicationSerializer,
 | 
			
		||||
    AppletHostAppletReportSerializer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HostMixin:
 | 
			
		||||
    request: Request
 | 
			
		||||
    permission_denied: Callable
 | 
			
		||||
    kwargs: dict
 | 
			
		||||
    rbac_perms = (
 | 
			
		||||
        ('list', 'terminal.view_applethost'),
 | 
			
		||||
        ('retrieve', 'terminal.view_applethost'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def get_permissions(self):
 | 
			
		||||
        if self.kwargs.get('host') and settings.DEBUG:
 | 
			
		||||
            return [RBACPermission()]
 | 
			
		||||
        else:
 | 
			
		||||
            return [IsServiceAccount()]
 | 
			
		||||
 | 
			
		||||
    def self_host(self):
 | 
			
		||||
        try:
 | 
			
		||||
            return self.request.user.terminal.applet_host
 | 
			
		||||
        except AttributeError:
 | 
			
		||||
            raise self.permission_denied(self.request, 'User has no applet host')
 | 
			
		||||
 | 
			
		||||
    def pk_host(self):
 | 
			
		||||
        return get_object_or_404(AppletHost, id=self.kwargs.get('host'))
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def host(self):
 | 
			
		||||
        if self.kwargs.get('host'):
 | 
			
		||||
            return self.pk_host()
 | 
			
		||||
        else:
 | 
			
		||||
            return self.self_host()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppletHostAccountsViewSet(HostMixin, JMSModelViewSet):
 | 
			
		||||
    serializer_class = AppletHostAccountSerializer
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        with tmp_to_builtin_org(system=1):
 | 
			
		||||
            queryset = self.host.accounts.all()
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppletHostAppletViewSet(HostMixin, JMSModelViewSet):
 | 
			
		||||
    host: AppletHost
 | 
			
		||||
    serializer_class = AppletPublicationSerializer
 | 
			
		||||
 | 
			
		||||
    def get_object(self):
 | 
			
		||||
        pk = self.kwargs.get('pk')
 | 
			
		||||
        if not is_uuid(pk):
 | 
			
		||||
            return self.host.publications.get(applet__name=pk)
 | 
			
		||||
        else:
 | 
			
		||||
            return self.host.publications.get(pk=pk)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        queryset = self.host.publications.all()
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    @action(methods=['post'], detail=False)
 | 
			
		||||
    def reports(self, request, *args, **kwargs):
 | 
			
		||||
        serializer = AppletHostAppletReportSerializer(data=request.data, many=True)
 | 
			
		||||
        serializer.is_valid(raise_exception=True)
 | 
			
		||||
        data = serializer.validated_data
 | 
			
		||||
        self.host.check_applets_state(data)
 | 
			
		||||
        publications = self.host.publications.all()
 | 
			
		||||
        serializer = AppletPublicationSerializer(publications, many=True)
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
    RDS_fSingleSessionPerUser: 1
 | 
			
		||||
    RDS_MaxDisconnectionTime: 60000
 | 
			
		||||
    RDS_RemoteAppLogoffTimeLimit: 0
 | 
			
		||||
    TinkerInstaller: JumpServer-Remoteapp_v0.0.1.exe
 | 
			
		||||
    TinkerInstaller: Tinker_Installer_v0.0.1.exe
 | 
			
		||||
 | 
			
		||||
  tasks:
 | 
			
		||||
  - name: Install RDS-Licensing (RDS)
 | 
			
		||||
| 
						 | 
				
			
			@ -31,12 +31,12 @@
 | 
			
		|||
      include_management_tools: yes
 | 
			
		||||
    register: rds_install
 | 
			
		||||
 | 
			
		||||
  - name: Download JumpServer Remoteapp installer (jumpserver)
 | 
			
		||||
  - name: Download JumpServer Tinker installer (jumpserver)
 | 
			
		||||
    ansible.windows.win_get_url:
 | 
			
		||||
        url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
 | 
			
		||||
        dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
 | 
			
		||||
 | 
			
		||||
  - name: Install JumpServer Remoteapp agent (jumpserver)
 | 
			
		||||
  - name: Install JumpServer Tinker (jumpserver)
 | 
			
		||||
    ansible.windows.win_package:
 | 
			
		||||
      path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
 | 
			
		||||
      arguments:
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +48,7 @@
 | 
			
		|||
  - name: Set remote-server on the global system path (remote-server)
 | 
			
		||||
    ansible.windows.win_path:
 | 
			
		||||
      elements:
 | 
			
		||||
        - '%USERPROFILE%\AppData\Local\Programs\JumpServer-Remoteapp\'
 | 
			
		||||
        - '%USERPROFILE%\AppData\Local\Programs\Tinker\'
 | 
			
		||||
      scope: user
 | 
			
		||||
 | 
			
		||||
  - name: Download python-3.10.8
 | 
			
		||||
| 
						 | 
				
			
			@ -153,18 +153,18 @@
 | 
			
		|||
      arguments:
 | 
			
		||||
      - /quiet
 | 
			
		||||
 | 
			
		||||
  - name: Generate component config
 | 
			
		||||
  - name: Generate tinkerd component config
 | 
			
		||||
    ansible.windows.win_shell:
 | 
			
		||||
      "remoteapp-server config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} 
 | 
			
		||||
      "tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} 
 | 
			
		||||
      --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}"
 | 
			
		||||
 | 
			
		||||
  - name: Install remoteapp-server service
 | 
			
		||||
  - name: Install tinkerd service
 | 
			
		||||
    ansible.windows.win_shell:
 | 
			
		||||
      "remoteapp-server service install"
 | 
			
		||||
      "tinkerd service install"
 | 
			
		||||
 | 
			
		||||
  - name: Start remoteapp-server service
 | 
			
		||||
  - name: Start tinkerd service
 | 
			
		||||
    ansible.windows.win_shell:
 | 
			
		||||
      "remoteapp-server  service start"
 | 
			
		||||
      "tinkerd service start"
 | 
			
		||||
 | 
			
		||||
  - name: Wait Tinker api health
 | 
			
		||||
    ansible.windows.win_uri:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,6 @@ from common.db.models import JMSBaseModel
 | 
			
		|||
from common.utils import random_string
 | 
			
		||||
from assets.models import Host
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['AppletHost', 'AppletHostDeployment']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +25,7 @@ class AppletHost(Host):
 | 
			
		|||
    )
 | 
			
		||||
    applets = models.ManyToManyField(
 | 
			
		||||
        'Applet', verbose_name=_('Applet'),
 | 
			
		||||
        through='AppletPublication',  through_fields=('host', 'applet'),
 | 
			
		||||
        through='AppletPublication', through_fields=('host', 'applet'),
 | 
			
		||||
    )
 | 
			
		||||
    LOCKING_ORG = '00000000-0000-0000-0000-000000000004'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,10 +33,10 @@ class AppletHost(Host):
 | 
			
		|||
        return self.name
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def status(self):
 | 
			
		||||
        if self.terminal:
 | 
			
		||||
            return 'online'
 | 
			
		||||
        return self.terminal.status
 | 
			
		||||
    def load(self):
 | 
			
		||||
        if not self.terminal:
 | 
			
		||||
            return 'offline'
 | 
			
		||||
        return self.terminal.load
 | 
			
		||||
 | 
			
		||||
    def check_terminal_binding(self, request):
 | 
			
		||||
        request_terminal = getattr(request.user, 'terminal', None)
 | 
			
		||||
| 
						 | 
				
			
			@ -70,8 +69,8 @@ class AppletHost(Host):
 | 
			
		|||
                status_applets['published'].append(applet)
 | 
			
		||||
 | 
			
		||||
        for status, applets in status_applets.items():
 | 
			
		||||
            self.publications.filter(applet__in=applets)\
 | 
			
		||||
                .exclude(status=status)\
 | 
			
		||||
            self.publications.filter(applet__in=applets) \
 | 
			
		||||
                .exclude(status=status) \
 | 
			
		||||
                .update(status=status)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +94,7 @@ class AppletHost(Host):
 | 
			
		|||
            account = account_model(
 | 
			
		||||
                username=username, secret=password, name=username,
 | 
			
		||||
                asset_id=self.id, secret_type='password', version=1,
 | 
			
		||||
                org_id=self.LOCKING_ORG
 | 
			
		||||
                org_id=self.LOCKING_ORG, is_active=False,
 | 
			
		||||
            )
 | 
			
		||||
            accounts.append(account)
 | 
			
		||||
        bulk_create_with_history(accounts, account_model, batch_size=20)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import uuid
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.db import models
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +35,7 @@ class TerminalStatusMixin:
 | 
			
		|||
    def is_alive(self):
 | 
			
		||||
        if not self.last_stat:
 | 
			
		||||
            return False
 | 
			
		||||
        return self.last_stat.date_created > timezone.now() - timezone.timedelta(seconds=120)
 | 
			
		||||
        return time.time() - self.last_stat.date_created.timestamp() < 150
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StorageMixin:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ class AppletPublicationSerializer(serializers.ModelSerializer):
 | 
			
		|||
        UNPUBLISHED = 'unpublished', _('Unpublished')
 | 
			
		||||
        NOT_MATCH = 'not_match', _('Not match')
 | 
			
		||||
 | 
			
		||||
    applet = ObjectRelatedField(attrs=('id', 'display_name', 'icon', 'version'), queryset=Applet.objects.all())
 | 
			
		||||
    applet = ObjectRelatedField(attrs=('id', 'name', 'display_name', 'icon', 'version'), queryset=Applet.objects.all())
 | 
			
		||||
    host = ObjectRelatedField(queryset=AppletHost.objects.all())
 | 
			
		||||
    status = LabeledChoiceField(choices=Status.choices, label=_("Status"))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,16 +2,18 @@ from rest_framework import serializers
 | 
			
		|||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from common.validators import ProjectUniqueValidator
 | 
			
		||||
from common.drf.fields import ObjectRelatedField
 | 
			
		||||
from common.drf.fields import ObjectRelatedField, LabeledChoiceField
 | 
			
		||||
from assets.models import Platform, Account
 | 
			
		||||
from assets.serializers import HostSerializer
 | 
			
		||||
from ..models import AppletHost, AppletHostDeployment, Applet
 | 
			
		||||
from .applet import AppletSerializer
 | 
			
		||||
from .. import const
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'AppletHostSerializer', 'AppletHostDeploymentSerializer',
 | 
			
		||||
    'AppletHostAccountSerializer', 'AppletHostReportSerializer'
 | 
			
		||||
    'AppletHostAccountSerializer', 'AppletHostAppletReportSerializer',
 | 
			
		||||
    'AppletHostStartupSerializer',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,14 +36,16 @@ class DeployOptionsSerializer(serializers.Serializer):
 | 
			
		|||
 | 
			
		||||
class AppletHostSerializer(HostSerializer):
 | 
			
		||||
    deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options"))
 | 
			
		||||
    load = LabeledChoiceField(
 | 
			
		||||
        read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta(HostSerializer.Meta):
 | 
			
		||||
        model = AppletHost
 | 
			
		||||
        fields = HostSerializer.Meta.fields + [
 | 
			
		||||
            'status', 'date_synced', 'deploy_options'
 | 
			
		||||
            'load', 'date_synced', 'deploy_options'
 | 
			
		||||
        ]
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'status': {'read_only': True},
 | 
			
		||||
            'date_synced': {'read_only': True}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -93,8 +97,14 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer):
 | 
			
		|||
class AppletHostAccountSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Account
 | 
			
		||||
        fields = ['id', 'username', 'secret', 'date_updated']
 | 
			
		||||
        fields = ['id', 'username', 'secret', 'is_active', 'date_updated']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppletHostReportSerializer(serializers.Serializer):
 | 
			
		||||
    applets = ObjectRelatedField(attrs=('id', 'name', 'version'), queryset=Applet.objects.all(), many=True)
 | 
			
		||||
class AppletHostAppletReportSerializer(serializers.Serializer):
 | 
			
		||||
    id = serializers.UUIDField(read_only=True)
 | 
			
		||||
    name = serializers.CharField()
 | 
			
		||||
    version = serializers.CharField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppletHostStartupSerializer(serializers.Serializer):
 | 
			
		||||
    pass
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
from django.db.models.signals import post_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
 | 
			
		||||
from orgs.utils import tmp_to_builtin_org
 | 
			
		||||
from .models import Applet, AppletHost
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +14,8 @@ def on_applet_host_create(sender, instance, created=False, **kwargs):
 | 
			
		|||
        return
 | 
			
		||||
    applets = Applet.objects.all()
 | 
			
		||||
    instance.applets.set(applets)
 | 
			
		||||
    instance.generate_accounts()
 | 
			
		||||
    with tmp_to_builtin_org(system=1):
 | 
			
		||||
        instance.generate_accounts()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=Applet)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,6 @@ class BaseTerminal(object):
 | 
			
		|||
 | 
			
		||||
            try:
 | 
			
		||||
                status = status_serializer.save()
 | 
			
		||||
                print("Save status ok: ", status)
 | 
			
		||||
                time.sleep(self.interval)
 | 
			
		||||
            except OperationalError:
 | 
			
		||||
                print("Save status error, close old connections")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,8 +12,8 @@ app_name = 'terminal'
 | 
			
		|||
 | 
			
		||||
router = BulkRouter()
 | 
			
		||||
router.register(r'sessions', api.SessionViewSet, 'session')
 | 
			
		||||
router.register(r'terminals/(?P<terminal>[a-zA-Z0-9\-]{36})?/?status', api.StatusViewSet, 'terminal-status')
 | 
			
		||||
router.register(r'terminals/(?P<terminal>[a-zA-Z0-9\-]{36})?/?sessions', api.SessionViewSet, 'terminal-sessions')
 | 
			
		||||
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
 | 
			
		||||
router.register(r'terminals/((?P<terminal>[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions')
 | 
			
		||||
router.register(r'terminals', api.TerminalViewSet, 'terminal')
 | 
			
		||||
router.register(r'tasks', api.TaskViewSet, 'tasks')
 | 
			
		||||
router.register(r'commands', api.CommandViewSet, 'command')
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +25,8 @@ router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session
 | 
			
		|||
router.register(r'endpoints', api.EndpointViewSet, 'endpoint')
 | 
			
		||||
router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
 | 
			
		||||
router.register(r'applets', api.AppletViewSet, 'applet')
 | 
			
		||||
router.register(r'applet-hosts/((?P<host>[^/.]+)/)?accounts', api.AppletHostAccountsViewSet, 'applet-host-account')
 | 
			
		||||
router.register(r'applet-hosts/((?P<host>[^/.]+)/)?applets', api.AppletHostAppletViewSet, 'applet-host-applet')
 | 
			
		||||
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
 | 
			
		||||
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
 | 
			
		||||
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,21 +5,24 @@ from rest_framework import serializers
 | 
			
		|||
from orgs.models import Organization
 | 
			
		||||
from orgs.utils import get_current_org_id
 | 
			
		||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
 | 
			
		||||
from common.drf.fields import LabeledChoiceField
 | 
			
		||||
from tickets.models import TicketFlow, ApprovalRule
 | 
			
		||||
from tickets.const import TicketApprovalStrategy
 | 
			
		||||
from tickets.const import TicketApprovalStrategy, TicketType
 | 
			
		||||
 | 
			
		||||
__all__ = ['TicketFlowSerializer']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketFlowApproveSerializer(serializers.ModelSerializer):
 | 
			
		||||
    strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
 | 
			
		||||
    strategy = LabeledChoiceField(
 | 
			
		||||
        choices=TicketApprovalStrategy.choices, required=True, label=_('Approve strategy')
 | 
			
		||||
    )
 | 
			
		||||
    assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
 | 
			
		||||
    assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ApprovalRule
 | 
			
		||||
        fields_small = [
 | 
			
		||||
            'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
 | 
			
		||||
            'level', 'strategy', 'assignees_read_only', 'assignees_display',
 | 
			
		||||
        ]
 | 
			
		||||
        fields_m2m = ['assignees', ]
 | 
			
		||||
        fields = fields_small + fields_m2m
 | 
			
		||||
| 
						 | 
				
			
			@ -46,14 +49,16 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
 | 
			
		||||
    type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
 | 
			
		||||
    type = LabeledChoiceField(
 | 
			
		||||
        choices=TicketType.choices, required=True, label=_('Type')
 | 
			
		||||
    )
 | 
			
		||||
    rules = TicketFlowApproveSerializer(many=True, required=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = TicketFlow
 | 
			
		||||
        fields_mini = ['id', ]
 | 
			
		||||
        fields_small = fields_mini + [
 | 
			
		||||
            'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated',
 | 
			
		||||
            'type', 'approval_level', 'created_by', 'date_created', 'date_updated',
 | 
			
		||||
            'org_id', 'org_name'
 | 
			
		||||
        ]
 | 
			
		||||
        fields = fields_small + ['rules', ]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue