diff --git a/.gitignore b/.gitignore index 3f398175f..316630d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ release/* releashe /apps/script.py data/* - +test.py diff --git a/Dockerfile b/Dockerfile index cf8f73a29..25ecaac3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,7 +99,6 @@ VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 -ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules EXPOSE 8080 diff --git a/apps/assets/automations/backup_account/__init__.py b/apps/accounts/__init__.py similarity index 100% rename from apps/assets/automations/backup_account/__init__.py rename to apps/accounts/__init__.py diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/accounts/api/__init__.py b/apps/accounts/api/__init__.py new file mode 100644 index 000000000..e49a88b3d --- /dev/null +++ b/apps/accounts/api/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .automations import * diff --git a/apps/assets/api/account/__init__.py b/apps/accounts/api/account/__init__.py similarity index 100% rename from apps/assets/api/account/__init__.py rename to apps/accounts/api/account/__init__.py diff --git a/apps/assets/api/account/account.py b/apps/accounts/api/account/account.py similarity index 92% rename from apps/assets/api/account/account.py rename to apps/accounts/api/account/account.py index 8dad13b3c..9aa31c72d 100644 --- a/apps/assets/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -3,12 +3,13 @@ from rest_framework.decorators import action from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.response import Response -from assets import serializers -from assets.filters import AccountFilterSet -from assets.models import Account, Asset -from assets.tasks import verify_accounts_connectivity +from accounts import serializers +from accounts.filters import AccountFilterSet +from accounts.models import Account +from accounts.tasks import verify_accounts_connectivity +from assets.models import Asset from authentication.const import ConfirmType -from common.mixins import RecordViewLogMixin +from common.views.mixins import RecordViewLogMixin from common.permissions import UserConfirmation from orgs.mixins.api import OrgBulkModelViewSet @@ -25,10 +26,9 @@ class AccountViewSet(OrgBulkModelViewSet): filterset_class = AccountFilterSet serializer_classes = { 'default': serializers.AccountSerializer, - 'verify': serializers.AssetTaskSerializer } rbac_perms = { - 'verify': 'assets.test_account', + 'verify_account': 'assets.test_account', 'partial_update': 'assets.change_accountsecret', 'su_from_accounts': 'assets.view_account', } diff --git a/apps/assets/api/account/backup.py b/apps/accounts/api/account/backup.py similarity index 70% rename from apps/assets/api/account/backup.py rename to apps/accounts/api/account/backup.py index ff46c3caf..216cbd585 100644 --- a/apps/assets/api/account/backup.py +++ b/apps/accounts/api/account/backup.py @@ -5,10 +5,10 @@ from rest_framework.response import Response from orgs.mixins.api import OrgBulkModelViewSet from common.const.choices import Trigger -from assets import serializers -from assets.tasks import execute_account_backup_plan -from assets.models import ( - AccountBackupPlan, AccountBackupPlanExecution +from accounts import serializers +from accounts.tasks import execute_account_backup_plan +from accounts.models import ( + AccountBackupAutomation, AccountBackupExecution ) __all__ = [ @@ -17,12 +17,12 @@ __all__ = [ class AccountBackupPlanViewSet(OrgBulkModelViewSet): - model = AccountBackupPlan + model = AccountBackupAutomation filter_fields = ('name',) search_fields = filter_fields ordering_fields = ('name',) ordering = ('name',) - serializer_class = serializers.AccountBackupPlanSerializer + serializer_class = serializers.AccountBackupSerializer class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): @@ -32,7 +32,7 @@ class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'post', 'options'] def get_queryset(self): - queryset = AccountBackupPlanExecution.objects.all() + queryset = AccountBackupExecution.objects.all() return queryset def create(self, request, *args, **kwargs): @@ -41,8 +41,3 @@ class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): pid = serializer.data.get('plan') task = execute_account_backup_plan.delay(pid=pid, trigger=Trigger.manual) return Response({'task': task.id}, status=status.HTTP_201_CREATED) - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = queryset.order_by('-date_start') - return queryset diff --git a/apps/assets/api/account/template.py b/apps/accounts/api/account/template.py similarity index 67% rename from apps/assets/api/account/template.py rename to apps/accounts/api/account/template.py index aa3be6b4f..cee8bac91 100644 --- a/apps/assets/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,7 +1,10 @@ -from assets import serializers -from assets.models import AccountTemplate -from common.mixins import RecordViewLogMixin +from rbac.permissions import RBACPermission +from common.permissions import UserConfirmation, ConfirmType + +from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet +from accounts import serializers +from accounts.models import AccountTemplate class AccountTemplateViewSet(OrgBulkModelViewSet): @@ -18,8 +21,7 @@ class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): 'default': serializers.AccountTemplateSecretSerializer, } http_method_names = ['get', 'options'] - # Todo: 记得打开 - # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] rbac_perms = { 'list': 'assets.view_accounttemplatesecret', 'retrieve': 'assets.view_accounttemplatesecret', diff --git a/apps/assets/api/automations/__init__.py b/apps/accounts/api/automations/__init__.py similarity index 74% rename from apps/assets/api/automations/__init__.py rename to apps/accounts/api/automations/__init__.py index e4daeda95..2b0aa0029 100644 --- a/apps/assets/api/automations/__init__.py +++ b/apps/accounts/api/automations/__init__.py @@ -1,3 +1,4 @@ from .base import * from .change_secret import * from .gather_accounts import * +from .push_account import * diff --git a/apps/assets/api/automations/base.py b/apps/accounts/api/automations/base.py similarity index 88% rename from apps/assets/api/automations/base.py rename to apps/accounts/api/automations/base.py index 23c2ef129..12fcd7b17 100644 --- a/apps/assets/api/automations/base.py +++ b/apps/accounts/api/automations/base.py @@ -4,8 +4,9 @@ from rest_framework import status, mixins, viewsets from rest_framework.response import Response from assets import serializers -from assets.models import BaseAutomation, AutomationExecution -from assets.tasks import execute_automation +from assets.models import BaseAutomation +from accounts.tasks import execute_automation +from accounts.models import AutomationExecution from common.const.choices import Trigger from orgs.mixins import generics @@ -17,13 +18,14 @@ __all__ = [ class AutomationAssetsListApi(generics.ListAPIView): + model = BaseAutomation 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) + return get_object_or_404(self.model, pk=pk) def get_queryset(self): instance = self.get_object() @@ -68,7 +70,7 @@ class AutomationAddAssetApi(generics.RetrieveUpdateAPIView): class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView): model = BaseAutomation - serializer_class = serializers.UpdateAssetSerializer + serializer_class = serializers.UpdateNodeSerializer def update(self, request, *args, **kwargs): action_params = ['add', 'remove'] @@ -97,21 +99,17 @@ class AutomationExecutionViewSet( filterset_fields = ('trigger', 'automation_id') serializer_class = serializers.AutomationExecutionSerializer + tp: str + 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') task = execute_automation.delay( - pid=automation.pk, trigger=Trigger.manual, tp=tp + pid=automation.pk, trigger=Trigger.manual, tp=self.tp ) return Response({'task': task.id}, status=status.HTTP_201_CREATED) diff --git a/apps/accounts/api/automations/change_secret.py b/apps/accounts/api/automations/change_secret.py new file mode 100644 index 000000000..b6034c79d --- /dev/null +++ b/apps/accounts/api/automations/change_secret.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import mixins + +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution +from common.utils import get_object_or_none +from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet +from .base import ( + AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, + AutomationNodeAddRemoveApi, AutomationExecutionViewSet +) + +__all__ = [ + 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', + 'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi', + 'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi', + 'ChangSecretNodeAddRemoveApi' +] + + +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.filter( + execution__automation__type=AutomationTypes.change_secret + ) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + eid = self.request.query_params.get('execution_id') + execution = get_object_or_none(AutomationExecution, pk=eid) + if execution: + queryset = queryset.filter(execution=execution) + return queryset + + +class ChangSecretExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "accounts.view_changesecretexecution"), + ("retrieve", "accounts.view_changesecretexecution"), + ("create", "accounts.add_changesecretexecution"), + ) + + tp = AutomationTypes.change_secret + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset + + +class ChangSecretAssetsListApi(AutomationAssetsListApi): + model = ChangeSecretAutomation + + +class ChangSecretRemoveAssetApi(AutomationRemoveAssetApi): + model = ChangeSecretAutomation + serializer_class = serializers.ChangeSecretUpdateAssetSerializer + + +class ChangSecretAddAssetApi(AutomationAddAssetApi): + model = ChangeSecretAutomation + serializer_class = serializers.ChangeSecretUpdateAssetSerializer + + +class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi): + model = ChangeSecretAutomation + serializer_class = serializers.ChangeSecretUpdateNodeSerializer diff --git a/apps/assets/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py similarity index 52% rename from apps/assets/api/automations/gather_accounts.py rename to apps/accounts/api/automations/gather_accounts.py index ba2d13df0..8fbd2dc92 100644 --- a/apps/assets/api/automations/gather_accounts.py +++ b/apps/accounts/api/automations/gather_accounts.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # -from assets import serializers -from assets.models import GatherAccountsAutomation +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import GatherAccountsAutomation from orgs.mixins.api import OrgBulkModelViewSet from .base import AutomationExecutionViewSet @@ -20,7 +21,14 @@ class GatherAccountsAutomationViewSet(OrgBulkModelViewSet): class GatherAccountsExecutionViewSet(AutomationExecutionViewSet): rbac_perms = ( - ("list", "assets.view_gatheraccountsexecution"), - ("retrieve", "assets.view_gatheraccountsexecution"), - ("create", "assets.add_gatheraccountsexecution"), + ("list", "accounts.view_gatheraccountsexecution"), + ("retrieve", "accounts.view_gatheraccountsexecution"), + ("create", "accounts.add_gatheraccountsexecution"), ) + + tp = AutomationTypes.gather_accounts + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset diff --git a/apps/accounts/api/automations/push_account.py b/apps/accounts/api/automations/push_account.py new file mode 100644 index 000000000..a736daaf6 --- /dev/null +++ b/apps/accounts/api/automations/push_account.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import PushAccountAutomation, ChangeSecretRecord +from orgs.mixins.api import OrgBulkModelViewSet + +from .base import ( + AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, + AutomationNodeAddRemoveApi, AutomationExecutionViewSet +) +from .change_secret import ChangeSecretRecordViewSet + +__all__ = [ + 'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi', + 'PushAccountAddAssetApi', 'PushAccountNodeAddRemoveApi', 'PushAccountExecutionViewSet', + 'PushAccountRecordViewSet' +] + + +class PushAccountAutomationViewSet(OrgBulkModelViewSet): + model = PushAccountAutomation + filter_fields = ('name', 'secret_type', 'secret_strategy') + search_fields = filter_fields + ordering_fields = ('name',) + serializer_class = serializers.PushAccountAutomationSerializer + + +class PushAccountExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "accounts.view_pushaccountexecution"), + ("retrieve", "accounts.view_pushaccountexecution"), + ("create", "accounts.add_pushaccountexecution"), + ) + + tp = AutomationTypes.push_account + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset + + +class PushAccountRecordViewSet(ChangeSecretRecordViewSet): + serializer_class = serializers.ChangeSecretRecordSerializer + + def get_queryset(self): + return ChangeSecretRecord.objects.filter( + execution__automation__type=AutomationTypes.push_account + ) + + +class PushAccountAssetsListApi(AutomationAssetsListApi): + model = PushAccountAutomation + + +class PushAccountRemoveAssetApi(AutomationRemoveAssetApi): + model = PushAccountAutomation + serializer_class = serializers.PushAccountUpdateAssetSerializer + + +class PushAccountAddAssetApi(AutomationAddAssetApi): + model = PushAccountAutomation + serializer_class = serializers.PushAccountUpdateAssetSerializer + + +class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi): + model = PushAccountAutomation + serializer_class = serializers.PushAccountUpdateNodeSerializer diff --git a/apps/accounts/apps.py b/apps/accounts/apps.py new file mode 100644 index 000000000..6fd8c2d9b --- /dev/null +++ b/apps/accounts/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' + + def ready(self): + from . import signal_handlers + __all__ = signal_handlers diff --git a/apps/accounts/automations/__init__.py b/apps/accounts/automations/__init__.py new file mode 100644 index 000000000..6f0f12d03 --- /dev/null +++ b/apps/accounts/automations/__init__.py @@ -0,0 +1,2 @@ +from .endpoint import ExecutionManager +from .methods import platform_automation_methods diff --git a/apps/assets/automations/change_secret/__init__.py b/apps/accounts/automations/backup_account/__init__.py similarity index 100% rename from apps/assets/automations/change_secret/__init__.py rename to apps/accounts/automations/backup_account/__init__.py diff --git a/apps/assets/automations/backup_account/handlers.py b/apps/accounts/automations/backup_account/handlers.py similarity index 97% rename from apps/assets/automations/backup_account/handlers.py rename to apps/accounts/automations/backup_account/handlers.py index 5c4ffce76..743a38c84 100644 --- a/apps/assets/automations/backup_account/handlers.py +++ b/apps/accounts/automations/backup_account/handlers.py @@ -7,10 +7,10 @@ from django.conf import settings from django.db.models import F from rest_framework import serializers -from assets.models import Account +from accounts.models import Account from assets.const import AllTypes -from assets.serializers import AccountSecretSerializer -from assets.notifications import AccountBackupExecutionTaskMsg +from accounts.serializers import AccountSecretSerializer +from accounts.notifications import AccountBackupExecutionTaskMsg from users.models import User from common.utils import get_logger from common.utils.timezone import local_now_display diff --git a/apps/assets/automations/backup_account/manager.py b/apps/accounts/automations/backup_account/manager.py similarity index 100% rename from apps/assets/automations/backup_account/manager.py rename to apps/accounts/automations/backup_account/manager.py diff --git a/apps/assets/automations/gather_accounts/__init__.py b/apps/accounts/automations/base/__init__.py similarity index 100% rename from apps/assets/automations/gather_accounts/__init__.py rename to apps/accounts/automations/base/__init__.py diff --git a/apps/accounts/automations/base/base_inventory.txt b/apps/accounts/automations/base/base_inventory.txt new file mode 100644 index 000000000..a2b73db16 --- /dev/null +++ b/apps/accounts/automations/base/base_inventory.txt @@ -0,0 +1,14 @@ +## all connection vars +hostname asset_name=name asset_type=type asset_primary_protocol=ssh asset_primary_port=22 asset_protocols=[] + +## local connection +hostname ansible_connection=local + +## local connection with gateway +hostname ansible_connection=ssh ansible_user=gateway.username ansible_port=gateway.port ansible_host=gateway.host ansible_ssh_private_key_file=gateway.key + +## ssh connection for windows +hostname ansible_connection=ssh ansible_shell_type=powershell/cmd ansible_user=windows.username ansible_port=windows.port ansible_host=windows.host ansible_ssh_private_key_file=windows.key + +## ssh connection +hostname ansible_user=user ansible_password=pass ansible_host=host ansible_port=port ansible_ssh_private_key_file=key ssh_args="-o StrictHostKeyChecking=no" diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py new file mode 100644 index 000000000..f39a1847e --- /dev/null +++ b/apps/accounts/automations/base/manager.py @@ -0,0 +1,55 @@ +from copy import deepcopy + +from common.utils import get_logger +from accounts.const import AutomationTypes, SecretType +from assets.automations.base.manager import BasePlaybookManager +from accounts.automations.methods import platform_automation_methods + +logger = get_logger(__name__) + + +class PushOrVerifyHostCallbackMixin: + execution: callable + get_accounts: callable + host_account_mapper: dict + generate_public_key: callable + generate_private_key_path: callable + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): + host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) + if host.get('error'): + return host + + accounts = asset.accounts.all() + accounts = self.get_accounts(account, accounts) + + inventory_hosts = [] + for account in accounts: + h = deepcopy(host) + h['name'] += '_' + account.username + self.host_account_mapper[h['name']] = account + secret = account.secret + + private_key_path = None + if account.secret_type == SecretType.SSH_KEY: + private_key_path = self.generate_private_key_path(secret, path_dir) + secret = self.generate_public_key(secret) + + h['secret_type'] = account.secret_type + h['account'] = { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': secret, + 'private_key_path': private_key_path + } + inventory_hosts.append(h) + return inventory_hosts + + +class AccountBasePlaybookManager(BasePlaybookManager): + pass + + @property + def platform_automation_methods(self): + return platform_automation_methods diff --git a/apps/assets/automations/push_account/__init__.py b/apps/accounts/automations/change_secret/__init__.py similarity index 100% rename from apps/assets/automations/push_account/__init__.py rename to apps/accounts/automations/change_secret/__init__.py diff --git a/apps/assets/automations/change_secret/database/mongodb/main.yml b/apps/accounts/automations/change_secret/database/mongodb/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/mongodb/main.yml rename to apps/accounts/automations/change_secret/database/mongodb/main.yml diff --git a/apps/assets/automations/change_secret/database/mongodb/manifest.yml b/apps/accounts/automations/change_secret/database/mongodb/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/mongodb/manifest.yml rename to apps/accounts/automations/change_secret/database/mongodb/manifest.yml index a59c0033b..b6eeaa139 100644 --- a/apps/assets/automations/change_secret/database/mongodb/manifest.yml +++ b/apps/accounts/automations/change_secret/database/mongodb/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_mongodb -name: Change password for MongoDB +name: Change secret for MongoDB category: database type: - mongodb diff --git a/apps/assets/automations/change_secret/database/mysql/main.yml b/apps/accounts/automations/change_secret/database/mysql/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/mysql/main.yml rename to apps/accounts/automations/change_secret/database/mysql/main.yml diff --git a/apps/assets/automations/change_secret/database/mysql/manifest.yml b/apps/accounts/automations/change_secret/database/mysql/manifest.yml similarity index 65% rename from apps/assets/automations/change_secret/database/mysql/manifest.yml rename to apps/accounts/automations/change_secret/database/mysql/manifest.yml index ca2aefc01..eac0fb56b 100644 --- a/apps/assets/automations/change_secret/database/mysql/manifest.yml +++ b/apps/accounts/automations/change_secret/database/mysql/manifest.yml @@ -1,6 +1,7 @@ id: change_secret_mysql -name: Change password for MySQL +name: Change secret for MySQL category: database type: - mysql + - mariadb method: change_secret diff --git a/apps/assets/automations/change_secret/database/oracle/main.yml b/apps/accounts/automations/change_secret/database/oracle/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/oracle/main.yml rename to apps/accounts/automations/change_secret/database/oracle/main.yml diff --git a/apps/assets/automations/change_secret/database/oracle/manifest.yml b/apps/accounts/automations/change_secret/database/oracle/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/oracle/manifest.yml rename to apps/accounts/automations/change_secret/database/oracle/manifest.yml index 19f109ba6..ee5cee177 100644 --- a/apps/assets/automations/change_secret/database/oracle/manifest.yml +++ b/apps/accounts/automations/change_secret/database/oracle/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_oracle -name: Change password for Oracle +name: Change secret for Oracle category: database type: - oracle diff --git a/apps/assets/automations/change_secret/database/postgresql/main.yml b/apps/accounts/automations/change_secret/database/postgresql/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/postgresql/main.yml rename to apps/accounts/automations/change_secret/database/postgresql/main.yml diff --git a/apps/assets/automations/change_secret/database/postgresql/manifest.yml b/apps/accounts/automations/change_secret/database/postgresql/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/postgresql/manifest.yml rename to apps/accounts/automations/change_secret/database/postgresql/manifest.yml index 48238f5ec..048637c7f 100644 --- a/apps/assets/automations/change_secret/database/postgresql/manifest.yml +++ b/apps/accounts/automations/change_secret/database/postgresql/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_postgresql -name: Change password for PostgreSQL +name: Change secret for PostgreSQL category: database type: - postgresql diff --git a/apps/assets/automations/change_secret/database/sqlserver/main.yml b/apps/accounts/automations/change_secret/database/sqlserver/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/sqlserver/main.yml rename to apps/accounts/automations/change_secret/database/sqlserver/main.yml diff --git a/apps/assets/automations/change_secret/database/sqlserver/manifest.yml b/apps/accounts/automations/change_secret/database/sqlserver/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/sqlserver/manifest.yml rename to apps/accounts/automations/change_secret/database/sqlserver/manifest.yml index 799c9e623..2a436e27f 100644 --- a/apps/assets/automations/change_secret/database/sqlserver/manifest.yml +++ b/apps/accounts/automations/change_secret/database/sqlserver/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_sqlserver -name: Change password for SQLServer +name: Change secret for SQLServer category: database type: - sqlserver diff --git a/apps/assets/automations/change_secret/demo_inventory.txt b/apps/accounts/automations/change_secret/demo_inventory.txt similarity index 100% rename from apps/assets/automations/change_secret/demo_inventory.txt rename to apps/accounts/automations/change_secret/demo_inventory.txt diff --git a/apps/assets/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml similarity index 100% rename from apps/assets/automations/change_secret/host/posix/main.yml rename to apps/accounts/automations/change_secret/host/posix/main.yml diff --git a/apps/assets/automations/change_secret/host/posix/manifest.yml b/apps/accounts/automations/change_secret/host/posix/manifest.yml similarity index 100% rename from apps/assets/automations/change_secret/host/posix/manifest.yml rename to apps/accounts/automations/change_secret/host/posix/manifest.yml diff --git a/apps/assets/automations/change_secret/host/windows/main.yml b/apps/accounts/automations/change_secret/host/windows/main.yml similarity index 100% rename from apps/assets/automations/change_secret/host/windows/main.yml rename to apps/accounts/automations/change_secret/host/windows/main.yml diff --git a/apps/assets/automations/change_secret/host/windows/manifest.yml b/apps/accounts/automations/change_secret/host/windows/manifest.yml similarity index 67% rename from apps/assets/automations/change_secret/host/windows/manifest.yml rename to apps/accounts/automations/change_secret/host/windows/manifest.yml index 80d0fb782..d727e3bec 100644 --- a/apps/assets/automations/change_secret/host/windows/manifest.yml +++ b/apps/accounts/automations/change_secret/host/windows/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_local_windows -name: Change password local account for Windows +name: Change secret local account for Windows version: 1 method: change_secret category: host diff --git a/apps/assets/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py similarity index 66% rename from apps/assets/automations/change_secret/manager.py rename to apps/accounts/automations/change_secret/manager.py index 30606956d..4360a9311 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -1,35 +1,38 @@ import os import time -import random -import string from copy import deepcopy from openpyxl import Workbook from collections import defaultdict -from django.utils import timezone from django.conf import settings +from django.utils import timezone -from common.utils.timezone import local_now_display -from common.utils.file import encrypt_and_compress_zip_file -from common.utils import get_logger, lazyproperty, gen_key_pair from users.models import User -from assets.models import ChangeSecretRecord -from assets.notifications import ChangeSecretExecutionTaskMsg -from assets.serializers import ChangeSecretRecordBackUpSerializer -from assets.const import ( - AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES -) -from ..base.manager import BasePlaybookManager +from accounts.models import ChangeSecretRecord +from accounts.notifications import ChangeSecretExecutionTaskMsg +from accounts.serializers import ChangeSecretRecordBackUpSerializer +from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy +from common.utils import get_logger, lazyproperty +from common.utils.file import encrypt_and_compress_zip_file +from common.utils.timezone import local_now_display +from ...utils import SecretGenerator +from ..base.manager import AccountBasePlaybookManager logger = get_logger(__name__) -class ChangeSecretManager(BasePlaybookManager): +class ChangeSecretManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.method_hosts_mapper = defaultdict(list) self.secret_type = self.execution.snapshot['secret_type'] - self.secret_strategy = self.execution.snapshot['secret_strategy'] + self.secret_strategy = self.execution.snapshot.get( + 'secret_strategy', SecretStrategy.custom + ) + self.ssh_key_change_strategy = self.execution.snapshot.get( + 'ssh_key_change_strategy', SSHKeyStrategy.add + ) + self.snapshot_account_usernames = self.execution.snapshot['accounts'] self._password_generated = None self._ssh_key_generated = None self.name_recorder_mapper = {} # 做个映射,方便后面处理 @@ -42,74 +45,31 @@ class ChangeSecretManager(BasePlaybookManager): def related_accounts(self): pass - @staticmethod - def generate_ssh_key(): - private_key, public_key = gen_key_pair() - return private_key - - def generate_password(self): - kwargs = self.execution.snapshot['password_rules'] or {} - length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) - symbol_set = kwargs.get('symbol_set') - if symbol_set is None: - symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] - - no_special_chars = string.ascii_letters + string.digits - chars = no_special_chars + symbol_set - - first_char = random.choice(no_special_chars) - password = ''.join([random.choice(chars) for _ in range(length - 1)]) - password = first_char + password - return password - - def get_ssh_key(self): - if self.secret_strategy == SecretStrategy.custom: - secret = self.execution.snapshot['secret'] - if not secret: - raise ValueError("Automation SSH key must be set") - return secret - elif self.secret_strategy == SecretStrategy.random_one: - if not self._ssh_key_generated: - self._ssh_key_generated = self.generate_ssh_key() - return self._ssh_key_generated - else: - return self.generate_ssh_key() - - def get_password(self): - if self.secret_strategy == SecretStrategy.custom: - password = self.execution.snapshot['secret'] - if not password: - raise ValueError("Automation Password must be set") - return password - elif self.secret_strategy == SecretStrategy.random_one: - if not self._password_generated: - self._password_generated = self.generate_password() - return self._password_generated - else: - return self.generate_password() - - def get_secret(self): - if self.secret_type == SecretType.SSH_KEY: - secret = self.get_ssh_key() - elif self.secret_type == SecretType.PASSWORD: - secret = self.get_password() - else: - raise ValueError("Secret must be set") - return secret - def get_kwargs(self, account, secret): kwargs = {} if self.secret_type != SecretType.SSH_KEY: return kwargs - kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy'] + kwargs['strategy'] = self.ssh_key_change_strategy kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' if kwargs['strategy'] == SSHKeyStrategy.set_jms: kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username) kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) - return kwargs + @lazyproperty + def secret_generator(self): + return SecretGenerator( + self.secret_strategy, self.secret_type, + self.execution.snapshot.get('password_rules') + ) + + def get_secret(self): + if self.secret_strategy == SecretStrategy.custom: + return self.execution.snapshot['secret'] + else: + return self.secret_generator.get_secret() + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) if host.get('error'): @@ -117,10 +77,10 @@ class ChangeSecretManager(BasePlaybookManager): accounts = asset.accounts.all() if account: - accounts = accounts.exclude(id=account.id) + accounts = accounts.exclude(username=account.username) - if '*' not in self.execution.snapshot['accounts']: - accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) + if '*' not in self.snapshot_account_usernames: + accounts = accounts.filter(username__in=self.snapshot_account_usernames) accounts = accounts.filter(secret_type=self.secret_type) method_attr = getattr(automation, self.method_type() + '_method') @@ -128,7 +88,6 @@ class ChangeSecretManager(BasePlaybookManager): method_hosts = [h for h in method_hosts if h != host['name']] inventory_hosts = [] records = [] - host['secret_type'] = self.secret_type for account in accounts: h = deepcopy(host) @@ -197,7 +156,8 @@ class ChangeSecretManager(BasePlaybookManager): recipients = self.execution.recipients if not recorders or not recipients: return - recipients = User.objects.filter(id__in=list(recipients)) + + recipients = User.objects.filter(id__in=list(recipients.keys())) name = self.execution.snapshot['name'] path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') @@ -219,7 +179,8 @@ class ChangeSecretManager(BasePlaybookManager): def create_file(recorders, filename): serializer_cls = ChangeSecretRecordBackUpSerializer serializer = serializer_cls(recorders, many=True) - header = [v.label for v in serializer.child.fields.values()] + + header = [str(v.label) for v in serializer.child.fields.values()] rows = [list(row.values()) for row in serializer.data] if not rows: return False diff --git a/apps/accounts/automations/endpoint.py b/apps/accounts/automations/endpoint.py new file mode 100644 index 000000000..2a1a821ea --- /dev/null +++ b/apps/accounts/automations/endpoint.py @@ -0,0 +1,24 @@ +from .change_secret.manager import ChangeSecretManager +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 ..const import AutomationTypes + + +class ExecutionManager: + manager_type_mapper = { + AutomationTypes.push_account: PushAccountManager, + AutomationTypes.change_secret: ChangeSecretManager, + AutomationTypes.verify_account: VerifyAccountManager, + AutomationTypes.gather_accounts: GatherAccountsManager, + # TODO 后期迁移到自动化策略中 + 'backup_account': AccountBackupManager, + } + + def __init__(self, execution): + self.execution = execution + self._runner = self.manager_type_mapper[execution.manager_type](execution) + + def run(self, *args, **kwargs): + return self._runner.run(*args, **kwargs) diff --git a/apps/assets/automations/verify_account/__init__.py b/apps/accounts/automations/gather_accounts/__init__.py similarity index 100% rename from apps/assets/automations/verify_account/__init__.py rename to apps/accounts/automations/gather_accounts/__init__.py diff --git a/apps/assets/automations/gather_accounts/database/mongodb/main.yml b/apps/accounts/automations/gather_accounts/database/mongodb/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/mongodb/main.yml rename to apps/accounts/automations/gather_accounts/database/mongodb/main.yml diff --git a/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml b/apps/accounts/automations/gather_accounts/database/mongodb/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/mongodb/manifest.yml rename to apps/accounts/automations/gather_accounts/database/mongodb/manifest.yml diff --git a/apps/assets/automations/gather_accounts/database/mysql/main.yml b/apps/accounts/automations/gather_accounts/database/mysql/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/mysql/main.yml rename to apps/accounts/automations/gather_accounts/database/mysql/main.yml diff --git a/apps/assets/automations/gather_accounts/database/mysql/manifest.yml b/apps/accounts/automations/gather_accounts/database/mysql/manifest.yml similarity index 90% rename from apps/assets/automations/gather_accounts/database/mysql/manifest.yml rename to apps/accounts/automations/gather_accounts/database/mysql/manifest.yml index e69cca67b..be104b783 100644 --- a/apps/assets/automations/gather_accounts/database/mysql/manifest.yml +++ b/apps/accounts/automations/gather_accounts/database/mysql/manifest.yml @@ -3,4 +3,5 @@ name: Gather account from MySQL category: database type: - mysql + - mariadb method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/database/oracle/main.yml b/apps/accounts/automations/gather_accounts/database/oracle/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/oracle/main.yml rename to apps/accounts/automations/gather_accounts/database/oracle/main.yml diff --git a/apps/assets/automations/gather_accounts/database/oracle/manifest.yml b/apps/accounts/automations/gather_accounts/database/oracle/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/oracle/manifest.yml rename to apps/accounts/automations/gather_accounts/database/oracle/manifest.yml diff --git a/apps/assets/automations/gather_accounts/database/postgresql/main.yml b/apps/accounts/automations/gather_accounts/database/postgresql/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/postgresql/main.yml rename to apps/accounts/automations/gather_accounts/database/postgresql/main.yml diff --git a/apps/assets/automations/gather_accounts/database/postgresql/manifest.yml b/apps/accounts/automations/gather_accounts/database/postgresql/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/postgresql/manifest.yml rename to apps/accounts/automations/gather_accounts/database/postgresql/manifest.yml diff --git a/apps/assets/automations/gather_accounts/filter.py b/apps/accounts/automations/gather_accounts/filter.py similarity index 100% rename from apps/assets/automations/gather_accounts/filter.py rename to apps/accounts/automations/gather_accounts/filter.py diff --git a/apps/assets/automations/gather_accounts/host/posix/main.yml b/apps/accounts/automations/gather_accounts/host/posix/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/posix/main.yml rename to apps/accounts/automations/gather_accounts/host/posix/main.yml diff --git a/apps/assets/automations/gather_accounts/host/posix/manifest.yml b/apps/accounts/automations/gather_accounts/host/posix/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/posix/manifest.yml rename to apps/accounts/automations/gather_accounts/host/posix/manifest.yml diff --git a/apps/assets/automations/gather_accounts/host/windows/main.yml b/apps/accounts/automations/gather_accounts/host/windows/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/windows/main.yml rename to apps/accounts/automations/gather_accounts/host/windows/main.yml diff --git a/apps/assets/automations/gather_accounts/host/windows/manifest.yml b/apps/accounts/automations/gather_accounts/host/windows/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/windows/manifest.yml rename to apps/accounts/automations/gather_accounts/host/windows/manifest.yml diff --git a/apps/assets/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py similarity index 93% rename from apps/assets/automations/gather_accounts/manager.py rename to apps/accounts/automations/gather_accounts/manager.py index da1b44abe..efbe1a965 100644 --- a/apps/assets/automations/gather_accounts/manager.py +++ b/apps/accounts/automations/gather_accounts/manager.py @@ -1,15 +1,15 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger -from assets.const import AutomationTypes, Source +from accounts.const import AutomationTypes, Source from orgs.utils import tmp_to_org from .filter import GatherAccountsFilter -from ..base.manager import BasePlaybookManager +from ..base.manager import AccountBasePlaybookManager logger = get_logger(__name__) -class GatherAccountsManager(BasePlaybookManager): +class GatherAccountsManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.host_asset_mapper = {} diff --git a/apps/accounts/automations/methods.py b/apps/accounts/automations/methods.py new file mode 100644 index 000000000..be5890701 --- /dev/null +++ b/apps/accounts/automations/methods.py @@ -0,0 +1,30 @@ +import os +import copy + +from accounts.const import AutomationTypes +from assets.automations.methods import get_platform_automation_methods + + +def copy_change_secret_to_push_account(methods): + push_account = AutomationTypes.push_account + change_secret = AutomationTypes.change_secret + copy_methods = copy.deepcopy(methods) + for method in copy_methods: + if not method['id'].startswith(change_secret): + continue + copy_method = copy.deepcopy(method) + copy_method['method'] = push_account.value + copy_method['id'] = copy_method['id'].replace( + change_secret, push_account + ) + copy_method['name'] = copy_method['name'].replace( + 'Change secret', 'Push account' + ) + methods.append(copy_method) + return methods + + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +automation_methods = get_platform_automation_methods(BASE_DIR) + +platform_automation_methods = copy_change_secret_to_push_account(automation_methods) diff --git a/apps/assets/backends/db.py b/apps/accounts/automations/push_account/__init__.py similarity index 100% rename from apps/assets/backends/db.py rename to apps/accounts/automations/push_account/__init__.py diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py new file mode 100644 index 000000000..896304db1 --- /dev/null +++ b/apps/accounts/automations/push_account/manager.py @@ -0,0 +1,112 @@ +from django.db.models import QuerySet + +from common.utils import get_logger +from accounts.const import AutomationTypes +from accounts.models import Account +from ..base.manager import PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager + +logger = get_logger(__name__) + + +class PushAccountManager(PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.secret_type = self.execution.snapshot['secret_type'] + self.host_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.push_account + + def create_nonlocal_accounts(self, accounts, snapshot_account_usernames, asset): + secret = self.execution.snapshot['secret'] + usernames = accounts.filter(secret_type=self.secret_type).values_list( + 'username', flat=True + ) + create_usernames = set(snapshot_account_usernames) - set(usernames) + create_account_objs = [ + Account( + name=username, username=username, secret=secret, + secret_type=self.secret_type, asset=asset, + ) + for username in create_usernames + ] + Account.objects.bulk_create(create_account_objs) + + def get_accounts(self, privilege_account, accounts: QuerySet): + if not privilege_account: + logger.debug(f'not privilege account') + return [] + snapshot_account_usernames = self.execution.snapshot['accounts'] + accounts = accounts.exclude(username=privilege_account.username) + if '*' in snapshot_account_usernames: + return accounts + + asset = privilege_account.asset + self.create_nonlocal_accounts(accounts, snapshot_account_usernames, asset) + accounts = asset.accounts.exclude(username=privilege_account.username).filter( + username__in=snapshot_account_usernames, secret_type=self.secret_type + ) + return accounts + + # @classmethod + # def trigger_by_asset_create(cls, asset): + # automations = PushAccountAutomation.objects.filter( + # triggers__contains=TriggerChoice.on_asset_create + # ) + # account_automation_map = {auto.username: auto for auto in automations} + # + # util = AssetPermissionUtil() + # permissions = util.get_permissions_for_assets([asset], with_node=True) + # account_permission_map = defaultdict(list) + # for permission in permissions: + # for account in permission.accounts: + # account_permission_map[account].append(permission) + # + # username_automation_map = {} + # for username, automation in account_automation_map.items(): + # if username != '@USER': + # username_automation_map[username] = automation + # continue + # + # asset_permissions = account_permission_map.get(username) + # if not asset_permissions: + # continue + # asset_permissions = util.get_permissions([p.id for p in asset_permissions]) + # usernames = asset_permissions.values_list('users__username', flat=True).distinct() + # for _username in usernames: + # username_automation_map[_username] = automation + # + # asset_usernames_exists = asset.accounts.values_list('username', flat=True) + # accounts_to_create = [] + # accounts_to_push = [] + # for username, automation in username_automation_map.items(): + # if username in asset_usernames_exists: + # continue + # + # if automation.secret_strategy != SecretStrategy.custom: + # secret_generator = SecretGenerator( + # automation.secret_strategy, automation.secret_type, + # automation.password_rules + # ) + # secret = secret_generator.get_secret() + # else: + # secret = automation.secret + # + # account = Account( + # username=username, secret=secret, + # asset=asset, secret_type=automation.secret_type, + # comment='Create by account creation {}'.format(automation.name), + # ) + # accounts_to_create.append(account) + # if automation.action == 'create_and_push': + # accounts_to_push.append(account) + # else: + # accounts_to_create.append(account) + # + # logger.debug(f'Create account {account} for asset {asset}') + + # @classmethod + # def trigger_by_permission_accounts_change(cls): + # pass diff --git a/apps/assets/serializers/account.py b/apps/accounts/automations/verify_account/__init__.py similarity index 100% rename from apps/assets/serializers/account.py rename to apps/accounts/automations/verify_account/__init__.py diff --git a/apps/assets/automations/verify_account/database/mongodb/main.yml b/apps/accounts/automations/verify_account/database/mongodb/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mongodb/main.yml rename to apps/accounts/automations/verify_account/database/mongodb/main.yml diff --git a/apps/assets/automations/verify_account/database/mongodb/manifest.yml b/apps/accounts/automations/verify_account/database/mongodb/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mongodb/manifest.yml rename to apps/accounts/automations/verify_account/database/mongodb/manifest.yml diff --git a/apps/assets/automations/verify_account/database/mysql/main.yml b/apps/accounts/automations/verify_account/database/mysql/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mysql/main.yml rename to apps/accounts/automations/verify_account/database/mysql/main.yml diff --git a/apps/assets/automations/verify_account/database/mysql/manifest.yml b/apps/accounts/automations/verify_account/database/mysql/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mysql/manifest.yml rename to apps/accounts/automations/verify_account/database/mysql/manifest.yml diff --git a/apps/assets/automations/verify_account/database/oracle/main.yml b/apps/accounts/automations/verify_account/database/oracle/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/oracle/main.yml rename to apps/accounts/automations/verify_account/database/oracle/main.yml diff --git a/apps/assets/automations/verify_account/database/oracle/manifest.yml b/apps/accounts/automations/verify_account/database/oracle/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/oracle/manifest.yml rename to apps/accounts/automations/verify_account/database/oracle/manifest.yml diff --git a/apps/assets/automations/verify_account/database/postgresql/main.yml b/apps/accounts/automations/verify_account/database/postgresql/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/postgresql/main.yml rename to apps/accounts/automations/verify_account/database/postgresql/main.yml diff --git a/apps/assets/automations/verify_account/database/postgresql/manifest.yml b/apps/accounts/automations/verify_account/database/postgresql/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/postgresql/manifest.yml rename to apps/accounts/automations/verify_account/database/postgresql/manifest.yml diff --git a/apps/assets/automations/verify_account/database/sqlserver/main.yml b/apps/accounts/automations/verify_account/database/sqlserver/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/sqlserver/main.yml rename to apps/accounts/automations/verify_account/database/sqlserver/main.yml diff --git a/apps/assets/automations/verify_account/database/sqlserver/manifest.yml b/apps/accounts/automations/verify_account/database/sqlserver/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/sqlserver/manifest.yml rename to apps/accounts/automations/verify_account/database/sqlserver/manifest.yml diff --git a/apps/assets/automations/verify_account/host/posix/main.yml b/apps/accounts/automations/verify_account/host/posix/main.yml similarity index 100% rename from apps/assets/automations/verify_account/host/posix/main.yml rename to apps/accounts/automations/verify_account/host/posix/main.yml diff --git a/apps/assets/automations/verify_account/host/posix/manifest.yml b/apps/accounts/automations/verify_account/host/posix/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/host/posix/manifest.yml rename to apps/accounts/automations/verify_account/host/posix/manifest.yml diff --git a/apps/assets/automations/verify_account/host/windows/main.yml b/apps/accounts/automations/verify_account/host/windows/main.yml similarity index 100% rename from apps/assets/automations/verify_account/host/windows/main.yml rename to apps/accounts/automations/verify_account/host/windows/main.yml diff --git a/apps/assets/automations/verify_account/host/windows/manifest.yml b/apps/accounts/automations/verify_account/host/windows/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/host/windows/manifest.yml rename to apps/accounts/automations/verify_account/host/windows/manifest.yml diff --git a/apps/assets/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py similarity index 51% rename from apps/assets/automations/verify_account/manager.py rename to apps/accounts/automations/verify_account/manager.py index f261631e5..12c247849 100644 --- a/apps/assets/automations/verify_account/manager.py +++ b/apps/accounts/automations/verify_account/manager.py @@ -1,12 +1,13 @@ +from django.db.models import QuerySet + from common.utils import get_logger -from assets.const import AutomationTypes, Connectivity -from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin +from accounts.const import AutomationTypes, Connectivity +from ..base.manager import PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager logger = get_logger(__name__) -class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): - need_privilege_account = False +class VerifyAccountManager(PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -16,6 +17,12 @@ class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): def method_type(cls): return AutomationTypes.verify_account + def get_accounts(self, privilege_account, accounts: QuerySet): + snapshot_account_usernames = self.execution.snapshot['accounts'] + if '*' not in snapshot_account_usernames: + accounts = accounts.filter(username__in=snapshot_account_usernames) + return accounts + def on_host_success(self, host, result): account = self.host_account_mapper.get(host) account.set_connectivity(Connectivity.OK) diff --git a/apps/accounts/const/__init__.py b/apps/accounts/const/__init__.py new file mode 100644 index 000000000..6db502556 --- /dev/null +++ b/apps/accounts/const/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .automation import * diff --git a/apps/assets/const/account.py b/apps/accounts/const/account.py similarity index 80% rename from apps/assets/const/account.py rename to apps/accounts/const/account.py index 4c15bb519..109044934 100644 --- a/apps/assets/const/account.py +++ b/apps/accounts/const/account.py @@ -2,12 +2,6 @@ from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ -class Connectivity(TextChoices): - UNKNOWN = 'unknown', _('Unknown') - OK = 'ok', _('Ok') - FAILED = 'failed', _('Failed') - - class SecretType(TextChoices): PASSWORD = 'password', _('Password') SSH_KEY = 'ssh_key', _('SSH key') diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py new file mode 100644 index 000000000..2c3b37bde --- /dev/null +++ b/apps/accounts/const/automation.py @@ -0,0 +1,94 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from assets.const import Connectivity +from common.db.fields import TreeChoices + +string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' +DEFAULT_PASSWORD_LENGTH = 30 +DEFAULT_PASSWORD_RULES = { + 'length': DEFAULT_PASSWORD_LENGTH, + 'symbol_set': string_punctuation +} + +__all__ = [ + 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', + 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', + 'PushAccountActionChoice', +] + + +class AutomationTypes(models.TextChoices): + push_account = 'push_account', _('Push account') + change_secret = 'change_secret', _('Change secret') + verify_account = 'verify_account', _('Verify account') + gather_accounts = 'gather_accounts', _('Gather accounts') + + @classmethod + def get_type_model(cls, tp): + from accounts.models import ( + PushAccountAutomation, ChangeSecretAutomation, + VerifyAccountAutomation, GatherAccountsAutomation, + ) + type_model_dict = { + cls.push_account: PushAccountAutomation, + cls.change_secret: ChangeSecretAutomation, + cls.verify_account: VerifyAccountAutomation, + cls.gather_accounts: GatherAccountsAutomation, + } + return type_model_dict.get(tp) + + +class SecretStrategy(models.TextChoices): + custom = 'specific', _('Specific password') + random = 'random', _('Random') + + +class SSHKeyStrategy(models.TextChoices): + add = 'add', _('Append SSH KEY') + set = 'set', _('Empty and append SSH KEY') + set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ') + + +class TriggerChoice(models.TextChoices, TreeChoices): + # 当资产创建时,直接创建账号,如果是动态账号,需要从授权中查询该资产被授权过的用户,已用户用户名为账号,创建 + on_asset_create = 'on_asset_create', _('On asset create') + # 授权变化包含,用户加入授权,用户组加入授权,资产加入授权,节点加入授权,账号变化 + # 当添加用户到授权时,查询所有同名账号 automation, 把本授权上的用户 (用户组), 创建到本授权的资产(节点)上 + on_perm_add_user = 'on_perm_add_user', _('On perm add user') + # 当添加用户组到授权时,查询所有同名账号 automation, 把本授权上的用户 (用户组), 创建到本授权的资产(节点)上 + on_perm_add_user_group = 'on_perm_add_user_group', _('On perm add user group') + # 当添加资产到授权时,查询授权的所有账号 automation, 创建到本授权的资产上 + on_perm_add_asset = 'on_perm_add_asset', _('On perm add asset') + # 当添加节点到授权时,查询授权的所有账号 automation, 创建到本授权的节点的资产上 + on_perm_add_node = 'on_perm_add_node', _('On perm add node') + # 当授权的账号变化时,查询授权的所有账号 automation, 创建到本授权的资产(节点)上 + on_perm_add_account = 'on_perm_add_account', _('On perm add account') + # 当资产添加到节点时,查询节点的授权规则,查询授权的所有账号 automation, 创建到本授权的资产(节点)上 + on_asset_join_node = 'on_asset_join_node', _('On asset join node') + # 当用户加入到用户组时,查询用户组的授权规则,查询授权的所有账号 automation, 创建到本授权的资产(节点)上 + on_user_join_group = 'on_user_join_group', _('On user join group') + + @classmethod + def branches(cls): + # 和用户和用户组相关的都是动态账号 + # + return [ + cls.on_asset_create, + (_("On perm change"), [ + cls.on_perm_add_user, + cls.on_perm_add_user_group, + cls.on_perm_add_asset, + cls.on_perm_add_node, + cls.on_perm_add_account, + ]), + (_("Inherit from group or node"), [ + cls.on_asset_join_node, + cls.on_user_join_group, + ]) + ] + + +class PushAccountActionChoice(models.TextChoices): + create_and_push = 'create_and_push', _('Create and push') + only_create = 'only_create', _('Only create') diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py new file mode 100644 index 000000000..28feecef4 --- /dev/null +++ b/apps/accounts/filters.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# +from django.db.models import Q +from django_filters import rest_framework as drf_filters + +from assets.models import Node +from common.drf.filters import BaseFilterSet + +from .models import Account + + +class AccountFilterSet(BaseFilterSet): + ip = drf_filters.CharFilter(field_name='address', lookup_expr='exact') + hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact') + username = drf_filters.CharFilter(field_name="username", lookup_expr='exact') + address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact') + asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact') + assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') + nodes = drf_filters.CharFilter(method='filter_nodes') + node_id = drf_filters.CharFilter(method='filter_nodes') + has_secret = drf_filters.BooleanFilter(method='filter_has_secret') + platform = drf_filters.CharFilter(field_name='asset__platform_id', lookup_expr='exact') + category = drf_filters.CharFilter(field_name='asset__platform__category', lookup_expr='exact') + type = drf_filters.CharFilter(field_name='asset__platform__type', lookup_expr='exact') + + @staticmethod + def filter_has_secret(queryset, name, has_secret): + q = Q(secret__isnull=True) | Q(secret='') + if has_secret: + return queryset.exclude(q) + else: + return queryset.filter(q) + + @staticmethod + def filter_nodes(queryset, name, value): + nodes = Node.objects.filter(id=value) + if not nodes: + return queryset + + node_qs = Node.objects.none() + for node in nodes: + node_qs |= node.get_all_children(with_self=True) + node_ids = list(node_qs.values_list('id', flat=True)) + queryset = queryset.filter(asset__nodes__in=node_ids) + return queryset + + class Meta: + model = Account + fields = ['id', 'asset_id'] diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py new file mode 100644 index 000000000..201a5141d --- /dev/null +++ b/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 3.2.14 on 2022-12-28 07:29 + +import common.db.encoder +import common.db.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0098_auto_20220430_2126'), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], + default='unknown', max_length=16, verbose_name='Connectivity')), + ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('version', models.IntegerField(default=0, verbose_name='Version')), + ('source', models.CharField(default='local', max_length=30, verbose_name='Source')), + ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', + to='assets.asset', verbose_name='Asset')), + ('su_from', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', + to='accounts.account', verbose_name='Su from')), + ], + options={ + 'verbose_name': 'Account', + 'permissions': [('view_accountsecret', 'Can view asset account secret'), + ('change_accountsecret', 'Can change asset account secret'), + ('view_historyaccount', 'Can view asset history account'), + ('view_historyaccountsecret', 'Can view asset history account secret')], + 'unique_together': {('username', 'asset', 'secret_type'), ('name', 'asset')}, + }, + ), + migrations.CreateModel( + name='HistoricalAccount', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('version', models.IntegerField(default=0, verbose_name='Version')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', + models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', + to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Account', + 'verbose_name_plural': 'historical Accounts', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='AccountTemplate', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ], + options={ + 'verbose_name': 'Account template', + 'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), + ('change_accounttemplatesecret', 'Can change asset account template secret')], + 'unique_together': {('name', 'org_id')}, + }, + ), + ] diff --git a/apps/accounts/migrations/0002_auto_20220616_0021.py b/apps/accounts/migrations/0002_auto_20220616_0021.py new file mode 100644 index 000000000..155800064 --- /dev/null +++ b/apps/accounts/migrations/0002_auto_20220616_0021.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:39 + +import common.db.encoder +import common.db.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0106_auto_20221228_1838'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AccountBackupAutomation', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('types', models.JSONField(default=list)), + ('recipients', models.ManyToManyField(blank=True, related_name='recipient_escape_route_plans', + to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Account backup plan', + 'ordering': ['name'], + 'unique_together': {('name', 'org_id')}, + }, + ) + ] diff --git a/apps/accounts/migrations/0003_automation.py b/apps/accounts/migrations/0003_automation.py new file mode 100644 index 000000000..503c766af --- /dev/null +++ b/apps/accounts/migrations/0003_automation.py @@ -0,0 +1,194 @@ +# Generated by Django 3.2.16 on 2022-12-30 08:08 + +import common.db.encoder +import common.db.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0107_automation'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0002_auto_20220616_0021'), + ] + + operations = [ + migrations.CreateModel( + name='AccountBaseAutomation', + fields=[ + ], + options={ + 'verbose_name': 'Account automation task', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='AutomationExecution', + fields=[ + ], + options={ + 'verbose_name': 'Automation execution', + 'verbose_name_plural': 'Automation executions', + 'permissions': [('view_changesecretexecution', 'Can view change secret execution'), + ('add_changesecretexection', 'Can add change secret execution'), + ('view_gatheraccountsexecution', 'Can view gather accounts execution'), + ('add_gatheraccountsexecution', 'Can add gather accounts execution')], + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.automationexecution',), + ), + migrations.CreateModel( + name='PushAccountAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret_strategy', models.CharField(choices=[('specific', 'Specific password'), + ('random_one', 'All assets use the same random password'), + ('random_all', + 'All assets use different random password')], + default='specific', max_length=16, + verbose_name='Secret strategy')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), + ('ssh_key_change_strategy', models.CharField( + choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), + ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, + verbose_name='SSH key change strategy')), + ('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')), + ('username', models.CharField(max_length=128, verbose_name='Username')), + ('action', models.CharField(max_length=16, verbose_name='Action')), + ], + options={ + 'verbose_name': 'Push asset account', + }, + bases=('accounts.accountbaseautomation', models.Model), + ), + + migrations.CreateModel( + name='GatherAccountsAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Gather asset accounts', + }, + bases=('accounts.accountbaseautomation',), + ), + migrations.CreateModel( + name='VerifyAccountAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Verify asset account', + }, + bases=('accounts.accountbaseautomation',), + ), + migrations.CreateModel( + name='ChangeSecretRecord', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')), + ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), + ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), + ('status', models.CharField(default='pending', max_length=16)), + ('error', models.TextField(blank=True, null=True, verbose_name='Error')), + ('account', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), + ('asset', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset')), + ('execution', + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.automationexecution')), + ], + options={ + 'verbose_name': 'Change secret record', + }, + ), + migrations.CreateModel( + name='AccountBackupExecution', + fields=[ + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')), + ('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')), + ('plan_snapshot', + models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, + verbose_name='Account backup snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], + default='manual', max_length=128, verbose_name='Trigger mode')), + ('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')), + ('is_success', models.BooleanField(default=False, verbose_name='Is success')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', + to='accounts.accountbackupautomation', verbose_name='Account backup plan')), + ], + options={ + 'verbose_name': 'Account backup execution', + 'ordering': ('-date_start',), + }, + ), + migrations.CreateModel( + name='ChangeSecretAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret_strategy', models.CharField(choices=[('specific', 'Specific password'), + ('random_one', 'All assets use the same random password'), + ('random_all', + 'All assets use different random password')], + default='specific', max_length=16, + verbose_name='Secret strategy')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), + ('ssh_key_change_strategy', models.CharField( + choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), + ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, + verbose_name='SSH key change strategy')), + ('recipients', + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Change secret automation', + }, + bases=('accounts.accountbaseautomation', models.Model), + ), + migrations.AlterModelOptions( + name='automationexecution', + options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), + ('add_changesecretexection', 'Can add change secret execution'), + ('view_gatheraccountsexecution', 'Can view gather accounts execution'), + ('add_gatheraccountsexecution', 'Can add gather accounts execution'), + ('view_pushaccountexecution', 'Can view push account execution'), + ('add_pushaccountexecution', 'Can add push account execution')], + 'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'}, + ), + migrations.AlterModelOptions( + name='changesecretrecord', + options={'ordering': ('-date_started',), 'verbose_name': 'Change secret record'}, + ), + ] diff --git a/apps/accounts/migrations/0004_auto_20230106_1507.py b/apps/accounts/migrations/0004_auto_20230106_1507.py new file mode 100644 index 000000000..3be64ef5a --- /dev/null +++ b/apps/accounts/migrations/0004_auto_20230106_1507.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2023-01-06 07:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_automation'), + ] + + operations = [ + migrations.AlterField( + model_name='changesecretautomation', + name='secret_strategy', + field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + ), + migrations.AlterField( + model_name='pushaccountautomation', + name='secret_strategy', + field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + ), + ] diff --git a/apps/accounts/migrations/0005_alter_changesecretrecord_options.py b/apps/accounts/migrations/0005_alter_changesecretrecord_options.py new file mode 100644 index 000000000..67971198f --- /dev/null +++ b/apps/accounts/migrations/0005_alter_changesecretrecord_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_auto_20230106_1507'), + ] + + operations = [ + migrations.AlterModelOptions( + name='changesecretrecord', + options={'ordering': ('-date_created',), 'verbose_name': 'Change secret record'}, + ), + ] diff --git a/apps/accounts/migrations/__init__.py b/apps/accounts/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/accounts/models/__init__.py b/apps/accounts/models/__init__.py new file mode 100644 index 000000000..c40ee786d --- /dev/null +++ b/apps/accounts/models/__init__.py @@ -0,0 +1,3 @@ +from .base import * +from .account import * +from .automations import * diff --git a/apps/assets/models/account.py b/apps/accounts/models/account.py similarity index 96% rename from apps/assets/models/account.py rename to apps/accounts/models/account.py index e0e87d37c..4b33a1d1a 100644 --- a/apps/assets/models/account.py +++ b/apps/accounts/models/account.py @@ -4,7 +4,8 @@ from simple_history.models import HistoricalRecords from common.utils import lazyproperty from ..const import AliasAccount, Source -from .base import AbsConnectivity, BaseAccount +from assets.models.base import AbsConnectivity +from .base import BaseAccount __all__ = ['Account', 'AccountTemplate'] @@ -46,7 +47,7 @@ class Account(AbsConnectivity, BaseAccount): on_delete=models.CASCADE, verbose_name=_('Asset') ) su_from = models.ForeignKey( - 'assets.Account', related_name='su_to', null=True, + 'accounts.Account', related_name='su_to', null=True, on_delete=models.SET_NULL, verbose_name=_("Su from") ) version = models.IntegerField(default=0, verbose_name=_('Version')) diff --git a/apps/accounts/models/automations/__init__.py b/apps/accounts/models/automations/__init__.py new file mode 100644 index 000000000..682b182b6 --- /dev/null +++ b/apps/accounts/models/automations/__init__.py @@ -0,0 +1,6 @@ +from .base import * +from .backup_account import * +from .change_secret import * +from .gather_account import * +from .push_account import * +from .verify_account import * diff --git a/apps/assets/models/backup.py b/apps/accounts/models/automations/backup_account.py similarity index 85% rename from apps/assets/models/backup.py rename to apps/accounts/models/automations/backup_account.py index d4a8b8cc8..473e2f3c0 100644 --- a/apps/assets/models/backup.py +++ b/apps/accounts/models/automations/backup_account.py @@ -13,12 +13,12 @@ from common.utils import get_logger from ops.mixin import PeriodTaskModelMixin from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel -__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution'] +__all__ = ['AccountBackupAutomation', 'AccountBackupExecution'] logger = get_logger(__file__) -class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): +class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): types = models.JSONField(default=list) recipients = models.ManyToManyField( 'users.User', related_name='recipient_escape_route_plans', blank=True, @@ -34,7 +34,7 @@ class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): verbose_name = _('Account backup plan') def get_register_task(self): - from ..tasks import execute_account_backup_plan + from ...tasks import execute_account_backup_plan name = "account_backup_plan_period_{}".format(str(self.id)[:8]) task = execute_account_backup_plan.name args = (str(self.id), Trigger.timing) @@ -56,18 +56,22 @@ class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): } } + @property + def executed_amount(self): + return self.execution.count() + def execute(self, trigger): try: hid = current_task.request.id except AttributeError: hid = str(uuid.uuid4()) - execution = AccountBackupPlanExecution.objects.create( + execution = AccountBackupExecution.objects.create( id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger ) return execution.start() -class AccountBackupPlanExecution(OrgModelMixin): +class AccountBackupExecution(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) date_start = models.DateTimeField( auto_now_add=True, verbose_name=_('Date start') @@ -88,11 +92,12 @@ class AccountBackupPlanExecution(OrgModelMixin): ) is_success = models.BooleanField(default=False, verbose_name=_('Is success')) plan = models.ForeignKey( - 'AccountBackupPlan', related_name='execution', on_delete=models.CASCADE, + 'AccountBackupAutomation', related_name='execution', on_delete=models.CASCADE, verbose_name=_('Account backup plan') ) class Meta: + ordering = ('-date_start',) verbose_name = _('Account backup execution') @property @@ -112,6 +117,6 @@ class AccountBackupPlanExecution(OrgModelMixin): return 'backup_account' def start(self): - from assets.automations.endpoint import ExecutionManager + from accounts.automations.endpoint import ExecutionManager manager = ExecutionManager(execution=self) return manager.run() diff --git a/apps/accounts/models/automations/base.py b/apps/accounts/models/automations/base.py new file mode 100644 index 000000000..ce45d6a81 --- /dev/null +++ b/apps/accounts/models/automations/base.py @@ -0,0 +1,41 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from assets.models.automations import ( + BaseAutomation as AssetBaseAutomation, + AutomationExecution as AssetAutomationExecution +) + +__all__ = ['AccountBaseAutomation', 'AutomationExecution'] + + +class AccountBaseAutomation(AssetBaseAutomation): + class Meta: + proxy = True + verbose_name = _("Account automation task") + + @property + def execution_model(self): + return AutomationExecution + + +class AutomationExecution(AssetAutomationExecution): + class Meta: + proxy = True + verbose_name = _("Automation execution") + verbose_name_plural = _("Automation executions") + permissions = [ + ('view_changesecretexecution', _('Can view change secret execution')), + ('add_changesecretexection', _('Can add change secret execution')), + + ('view_gatheraccountsexecution', _('Can view gather accounts execution')), + ('add_gatheraccountsexecution', _('Can add gather accounts execution')), + + ('view_pushaccountexecution', _('Can view push account execution')), + ('add_pushaccountexecution', _('Can add push account execution')), + ] + + def start(self): + from accounts.automations.endpoint import ExecutionManager + manager = ExecutionManager(execution=self) + return manager.run() diff --git a/apps/assets/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py similarity index 83% rename from apps/assets/models/automations/change_secret.py rename to apps/accounts/models/automations/change_secret.py index 7fb801cab..d52fc2cae 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/accounts/models/automations/change_secret.py @@ -1,10 +1,12 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy from common.db import fields from common.db.models import JMSBaseModel -from .base import BaseAutomation +from accounts.const import ( + AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy +) +from .base import AccountBaseAutomation __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin'] @@ -28,8 +30,20 @@ class ChangeSecretMixin(models.Model): class Meta: abstract = True + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + 'secret': self.secret, + 'secret_type': self.secret_type, + 'secret_strategy': self.secret_strategy, + 'password_rules': self.password_rules, + 'ssh_key_change_strategy': self.ssh_key_change_strategy, -class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): + }) + return attr_json + + +class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation): recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True) def save(self, *args, **kwargs): @@ -42,11 +56,6 @@ class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ - 'secret': self.secret, - 'secret_type': self.secret_type, - 'secret_strategy': self.secret_strategy, - 'password_rules': self.password_rules, - 'ssh_key_change_strategy': self.ssh_key_change_strategy, 'recipients': { str(recipient.id): (str(recipient), bool(recipient.secret_key)) for recipient in self.recipients.all() @@ -56,9 +65,9 @@ class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): class ChangeSecretRecord(JMSBaseModel): - execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE) + execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.CASCADE) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True) - account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, null=True) + account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True) old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret')) new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started')) @@ -67,6 +76,7 @@ class ChangeSecretRecord(JMSBaseModel): error = models.TextField(blank=True, null=True, verbose_name=_('Error')) class Meta: + ordering = ('-date_created',) verbose_name = _("Change secret record") def __str__(self): diff --git a/apps/assets/models/automations/gather_accounts.py b/apps/accounts/models/automations/gather_account.py similarity index 58% rename from apps/assets/models/automations/gather_accounts.py rename to apps/accounts/models/automations/gather_account.py index a3aa42383..1dd5500f7 100644 --- a/apps/assets/models/automations/gather_accounts.py +++ b/apps/accounts/models/automations/gather_account.py @@ -1,19 +1,15 @@ from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes -from .base import BaseAutomation +from accounts.const import AutomationTypes +from .base import AccountBaseAutomation __all__ = ['GatherAccountsAutomation'] -class GatherAccountsAutomation(BaseAutomation): +class GatherAccountsAutomation(AccountBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.gather_accounts super().save(*args, **kwargs) class Meta: verbose_name = _("Gather asset accounts") - - @property - def executed_amount(self): - return self.executions.count() diff --git a/apps/accounts/models/automations/push_account.py b/apps/accounts/models/automations/push_account.py new file mode 100644 index 000000000..4e4d95d9d --- /dev/null +++ b/apps/accounts/models/automations/push_account.py @@ -0,0 +1,41 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from accounts.const import AutomationTypes +from .base import AccountBaseAutomation +from .change_secret import ChangeSecretMixin + +__all__ = ['PushAccountAutomation'] + + +class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation): + accounts = None + triggers = models.JSONField(max_length=16, default=list, verbose_name=_('Triggers')) + username = models.CharField(max_length=128, verbose_name=_('Username')) + action = models.CharField(max_length=16, verbose_name=_('Action')) + + def set_period_schedule(self): + pass + + @property + def dynamic_username(self): + return self.username == '@USER' + + @dynamic_username.setter + def dynamic_username(self, value): + if value: + self.username = '@USER' + + def save(self, *args, **kwargs): + self.type = AutomationTypes.push_account + super().save(*args, **kwargs) + + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + 'username': self.username + }) + return attr_json + + class Meta: + verbose_name = _("Push asset account") diff --git a/apps/assets/models/automations/verify_account.py b/apps/accounts/models/automations/verify_account.py similarity index 67% rename from apps/assets/models/automations/verify_account.py rename to apps/accounts/models/automations/verify_account.py index cf7004820..03ec4f8c9 100644 --- a/apps/assets/models/automations/verify_account.py +++ b/apps/accounts/models/automations/verify_account.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes -from .base import BaseAutomation +from accounts.const import AutomationTypes +from .base import AccountBaseAutomation __all__ = ['VerifyAccountAutomation'] -class VerifyAccountAutomation(BaseAutomation): +class VerifyAccountAutomation(AccountBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.verify_account super().save(*args, **kwargs) diff --git a/apps/accounts/models/base.py b/apps/accounts/models/base.py new file mode 100644 index 000000000..ffae7051b --- /dev/null +++ b/apps/accounts/models/base.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +import os +from hashlib import md5 + +import sshpubkeys +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from accounts.const import SecretType +from common.db import fields +from common.utils import ( + ssh_key_string_to_obj, ssh_key_gen, get_logger, + random_string, lazyproperty, parse_ssh_public_key_str +) +from orgs.mixins.models import JMSOrgBaseModel, OrgManager + +logger = get_logger(__file__) + + +class BaseAccountQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + +class BaseAccountManager(OrgManager): + def active(self): + return self.get_queryset().active() + + +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') + ) + secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) + privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) + is_active = models.BooleanField(default=True, verbose_name=_("Is active")) + + objects = BaseAccountManager.from_queryset(BaseAccountQuerySet)() + + @property + def has_secret(self): + return bool(self.secret) + + @property + def has_username(self): + return bool(self.username) + + @property + def specific(self): + data = {} + if self.secret_type != SecretType.SSH_KEY: + return data + data['ssh_key_fingerprint'] = self.ssh_key_fingerprint + return data + + @property + def password(self): + if self.secret_type == SecretType.PASSWORD: + return self.secret + return None + + @property + def private_key(self): + 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 + + @lazyproperty + def public_key(self): + if self.secret_type == SecretType.SSH_KEY and self.private_key: + return parse_ssh_public_key_str(self.private_key) + return None + + @property + def ssh_key_fingerprint(self): + if self.public_key: + public_key = self.public_key + elif self.private_key: + try: + public_key = parse_ssh_public_key_str(self.private_key) + except IOError as e: + return str(e) + else: + return '' + + public_key_obj = sshpubkeys.SSHKey(public_key) + fingerprint = public_key_obj.hash_md5() + return fingerprint + + @property + def private_key_obj(self): + if self.private_key: + key_obj = ssh_key_string_to_obj(self.private_key) + return key_obj + else: + return None + + @property + def private_key_path(self): + if not self.secret_type != SecretType.SSH_KEY \ + or not self.secret \ + or not self.private_key: + return None + project_dir = settings.PROJECT_DIR + tmp_dir = os.path.join(project_dir, 'tmp') + key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() + key_path = os.path.join(tmp_dir, key_name) + if not os.path.exists(key_path): + self.private_key_obj.write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + + def get_private_key(self): + if not self.private_key: + return None + return self.private_key + + @property + def public_key_obj(self): + if self.public_key: + try: + return sshpubkeys.SSHKey(self.public_key) + except TabError: + pass + return None + + @staticmethod + def gen_password(length=36): + return random_string(length, special_char=True) + + @staticmethod + def gen_key(username): + private_key, public_key = ssh_key_gen(username=username) + return private_key, public_key + + def _to_secret_json(self): + """Push system user use it""" + return { + 'name': self.name, + 'username': self.username, + 'public_key': self.public_key, + } + + class Meta: + abstract = True diff --git a/apps/assets/notifications.py b/apps/accounts/notifications.py similarity index 61% rename from apps/assets/notifications.py rename to apps/accounts/notifications.py index a797bc845..3c4897df1 100644 --- a/apps/assets/notifications.py +++ b/apps/accounts/notifications.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ -from users.models import User from common.tasks import send_mail_attachment_async +from users.models import User class AccountBackupExecutionTaskMsg(object): @@ -15,11 +15,13 @@ class AccountBackupExecutionTaskMsg(object): def message(self): name = self.name if self.user.secret_key: - return _('{} - The account backup passage task has been completed. See the attachment for details').format( - name) - return _("{} - The account backup passage task has been completed: the encryption password has not been set - " - "please go to personal information -> file encryption password to set the encryption password").format( - name) + return _('{} - The account backup passage task has been completed.' + ' See the attachment for details').format(name) + else: + return _("{} - The account backup passage task has been completed: " + "the encryption password has not been set - " + "please go to personal information -> file encryption password " + "to set the encryption password").format(name) def publish(self, attachment_list=None): send_mail_attachment_async( @@ -38,10 +40,12 @@ class ChangeSecretExecutionTaskMsg(object): def message(self): name = self.name if self.user.secret_key: - return _('{} - The encryption change task has been completed. See the attachment for details').format(name) - return _("{} - The encryption change task has been completed: the encryption password has not been set - " - "please go to personal information -> file encryption password to set the encryption password").format( - name) + return _('{} - The encryption change task has been completed. ' + 'See the attachment for details').format(name) + else: + return _("{} - The encryption change task has been completed: the encryption " + "password has not been set - please go to personal information -> " + "file encryption password to set the encryption password").format(name) def publish(self, attachments=None): send_mail_attachment_async( diff --git a/apps/accounts/serializers/__init__.py b/apps/accounts/serializers/__init__.py new file mode 100644 index 000000000..e49a88b3d --- /dev/null +++ b/apps/accounts/serializers/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .automations import * diff --git a/apps/assets/serializers/account/__init__.py b/apps/accounts/serializers/account/__init__.py similarity index 77% rename from apps/assets/serializers/account/__init__.py rename to apps/accounts/serializers/account/__init__.py index 1e5dde298..725f00e94 100644 --- a/apps/assets/serializers/account/__init__.py +++ b/apps/accounts/serializers/account/__init__.py @@ -1,3 +1,4 @@ from .account import * -from .template import * from .backup import * +from .base import * +from .template import * diff --git a/apps/assets/serializers/account/account.py b/apps/accounts/serializers/account/account.py similarity index 92% rename from apps/assets/serializers/account/account.py rename to apps/accounts/serializers/account/account.py index c2eb786dd..43cc8e3d3 100644 --- a/apps/assets/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -1,11 +1,12 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from assets.const import SecretType, Source -from assets.models import Account, AccountTemplate, Asset -from assets.tasks import push_accounts_to_assets -from common.drf.fields import ObjectRelatedField, LabeledChoiceField -from common.drf.serializers import SecretReadableMixin, BulkModelSerializer +from assets.models import Asset +from accounts.const import SecretType, Source +from accounts.models import Account, AccountTemplate +from accounts.tasks import push_accounts_to_assets +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers import SecretReadableMixin, BulkModelSerializer from .base import BaseAccountSerializer diff --git a/apps/assets/serializers/account/backup.py b/apps/accounts/serializers/account/backup.py similarity index 58% rename from apps/assets/serializers/account/backup.py rename to apps/accounts/serializers/account/backup.py index 34121dadd..57d4c9ccc 100644 --- a/apps/assets/serializers/account/backup.py +++ b/apps/accounts/serializers/account/backup.py @@ -7,26 +7,28 @@ 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 common.serializers.fields import LabeledChoiceField -from assets.models import AccountBackupPlan, AccountBackupPlanExecution +from accounts.models import AccountBackupAutomation, AccountBackupExecution logger = get_logger(__file__) -__all__ = ['AccountBackupPlanSerializer', 'AccountBackupPlanExecutionSerializer'] +__all__ = ['AccountBackupSerializer', 'AccountBackupPlanExecutionSerializer'] -class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): +class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): class Meta: - model = AccountBackupPlan - fields = [ - 'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created', - 'date_updated', 'created_by', 'periodic_display', 'comment', - 'recipients', 'types' + model = AccountBackupAutomation + read_only_fields = [ + 'date_created', 'date_updated', 'created_by', 'periodic_display', 'executed_amount' + ] + fields = read_only_fields + [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', 'recipients', 'types' ] extra_kwargs = { 'name': {'required': True}, 'periodic_display': {'label': _('Periodic perform')}, + 'executed_amount': {'label': _('Executed amount')}, 'recipients': {'label': _('Recipient'), 'help_text': _( 'Currently only mail sending is supported' )} @@ -34,9 +36,10 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): + trigger = LabeledChoiceField(choices=Trigger.choices, label=_("Trigger mode")) class Meta: - model = AccountBackupPlanExecution + model = AccountBackupExecution read_only_fields = [ 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', 'is_success', 'org_id', 'recipients' diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py new file mode 100644 index 000000000..d92c4cc9c --- /dev/null +++ b/apps/accounts/serializers/account/base.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from accounts.const import SecretType +from accounts.models import BaseAccount +from accounts.utils import validate_password_for_ansible, validate_ssh_key +from common.serializers.fields import EncryptedField, LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer + +__all__ = ['AuthValidateMixin', 'BaseAccountSerializer'] + + +class AuthValidateMixin(serializers.Serializer): + secret_type = LabeledChoiceField( + choices=SecretType.choices, required=True, label=_('Secret type') + ) + 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') + ) + + @property + def initial_secret_type(self): + secret_type = self.initial_data.get('secret_type') + return secret_type + + 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 ('secret',): + value = validated_data.get(field) + if value is None: + validated_data.pop(field, None) + validated_data.pop('passphrase', None) + + def create(self, validated_data): + self.clean_auth_fields(validated_data) + return super().create(validated_data) + + def update(self, instance, validated_data): + self.clean_auth_fields(validated_data) + return super().update(instance, validated_data) + + +class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): + has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) + + class Meta: + model = BaseAccount + fields_mini = ['id', 'name', 'username'] + 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 = [ + 'has_secret', 'specific', + 'date_verified', 'created_by', 'date_created', + ] + extra_kwargs = { + 'specific': {'label': _('Specific')}, + } diff --git a/apps/assets/serializers/account/template.py b/apps/accounts/serializers/account/template.py similarity index 89% rename from apps/assets/serializers/account/template.py rename to apps/accounts/serializers/account/template.py index 9227b5585..a72565cf5 100644 --- a/apps/assets/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -1,5 +1,5 @@ -from assets.models import AccountTemplate -from common.drf.serializers import SecretReadableMixin +from accounts.models import AccountTemplate +from common.serializers import SecretReadableMixin from .base import BaseAccountSerializer diff --git a/apps/accounts/serializers/automations/__init__.py b/apps/accounts/serializers/automations/__init__.py new file mode 100644 index 000000000..2b0aa0029 --- /dev/null +++ b/apps/accounts/serializers/automations/__init__.py @@ -0,0 +1,4 @@ +from .base import * +from .change_secret import * +from .gather_accounts import * +from .push_account import * diff --git a/apps/accounts/serializers/automations/base.py b/apps/accounts/serializers/automations/base.py new file mode 100644 index 000000000..8c11e8966 --- /dev/null +++ b/apps/accounts/serializers/automations/base.py @@ -0,0 +1,83 @@ +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 +from accounts.models import AutomationExecution +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.utils import get_logger +from common.serializers.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', 'executed_amount' + ] + 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')}, + 'executed_amount': {'label': _('Executed amount')}, + } + + +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) diff --git a/apps/assets/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py similarity index 77% rename from apps/assets/serializers/automations/change_secret.py rename to apps/accounts/serializers/automations/change_secret.py index b0149334d..4fbd55801 100644 --- a/apps/assets/serializers/automations/change_secret.py +++ b/apps/accounts/serializers/automations/change_secret.py @@ -3,12 +3,18 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers +from accounts.const import ( + DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy +) +from accounts.models import ( + Account, ChangeSecretAutomation, + ChangeSecretRecord +) +from accounts.models import AutomationExecution +from accounts.serializers import AuthValidateMixin +from assets.models import Asset +from common.serializers.fields import LabeledChoiceField, ObjectRelatedField 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__) @@ -16,10 +22,19 @@ logger = get_logger(__file__) __all__ = [ 'ChangeSecretAutomationSerializer', 'ChangeSecretRecordSerializer', - 'ChangeSecretRecordBackUpSerializer' + 'ChangeSecretRecordBackUpSerializer', + 'ChangeSecretUpdateAssetSerializer', + 'ChangeSecretUpdateNodeSerializer', ] +def get_secret_types(): + return [ + (SecretType.PASSWORD, _('Password')), + (SecretType.SSH_KEY, _('SSH key')), + ] + + class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer): secret_strategy = LabeledChoiceField( choices=SecretStrategy.choices, required=True, label=_('Secret strategy') @@ -28,6 +43,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy') ) password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES) + secret_type = LabeledChoiceField(choices=get_secret_types(), required=True, label=_('Secret type')) class Meta: model = ChangeSecretAutomation @@ -42,24 +58,14 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ )}, }} - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_secret_type_choices() - - def set_secret_type_choices(self): - secret_type = self.fields.get('secret_type') - if not secret_type: - return - choices = secret_type._choices - choices.pop(SecretType.ACCESS_KEY, None) - choices.pop(SecretType.TOKEN, None) - secret_type._choices = choices - def validate_password_rules(self, password_rules): secret_type = self.initial_secret_type if secret_type != SecretType.PASSWORD: return password_rules + if self.initial_data.get('secret_strategy') == SecretStrategy.custom: + return password_rules + length = password_rules.get('length') symbol_set = password_rules.get('symbol_set', '') @@ -69,6 +75,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ 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) @@ -113,9 +120,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer): @staticmethod def get_is_success(obj): - if obj.status == 'success': - return _("Success") - return _("Failed") + return obj.status == 'success' class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer): @@ -144,3 +149,15 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer): if obj.status == 'success': return _("Success") return _("Failed") + + +class ChangeSecretUpdateAssetSerializer(serializers.ModelSerializer): + class Meta: + model = ChangeSecretAutomation + fields = ['id', 'assets'] + + +class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer): + class Meta: + model = ChangeSecretAutomation + fields = ['id', 'nodes'] diff --git a/apps/assets/serializers/automations/gather_accounts.py b/apps/accounts/serializers/automations/gather_accounts.py similarity index 68% rename from apps/assets/serializers/automations/gather_accounts.py rename to apps/accounts/serializers/automations/gather_accounts.py index 6b86fd64c..ffca89198 100644 --- a/apps/assets/serializers/automations/gather_accounts.py +++ b/apps/accounts/serializers/automations/gather_accounts.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ -from assets.models import GatherAccountsAutomation +from accounts.models import GatherAccountsAutomation from common.utils import get_logger from .base import BaseAutomationSerializer @@ -16,9 +16,7 @@ __all__ = [ class GatherAccountAutomationSerializer(BaseAutomationSerializer): class Meta: model = GatherAccountsAutomation - read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + ['executed_amount'] + read_only_fields = BaseAutomationSerializer.Meta.read_only_fields fields = BaseAutomationSerializer.Meta.fields + read_only_fields - extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{ - 'executed_amount': {'label': _('Executed amount')} - }} + extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs diff --git a/apps/accounts/serializers/automations/push_account.py b/apps/accounts/serializers/automations/push_account.py new file mode 100644 index 000000000..aad6041ba --- /dev/null +++ b/apps/accounts/serializers/automations/push_account.py @@ -0,0 +1,73 @@ +import copy +from accounts.models import PushAccountAutomation +from .change_secret import ( + ChangeSecretAutomationSerializer, ChangeSecretUpdateAssetSerializer, + ChangeSecretUpdateNodeSerializer +) + + +class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer): + # dynamic_username = serializers.BooleanField(label=_('Dynamic username'), default=False) + # triggers = TreeChoicesField( + # choice_cls=TriggerChoice, label=_('Triggers'), + # default=TriggerChoice.all(), + # ) + # action = LabeledChoiceField( + # choices=PushAccountActionChoice.choices, label=_('Action'), + # default=PushAccountActionChoice.create_and_push + # ) + + class Meta(ChangeSecretAutomationSerializer.Meta): + model = PushAccountAutomation + fields = copy.copy(ChangeSecretAutomationSerializer.Meta.fields) + fields.remove('recipients') + + # fields = ChangeSecretAutomationSerializer.Meta.fields + [ + # 'dynamic_username', 'triggers', 'action' + # ] + + # def validate_username(self, value): + # if self.initial_data.get('dynamic_username'): + # value = '@USER' + # queryset = self.Meta.model.objects.filter(username=value) + # if self.instance: + # queryset = queryset.exclude(id=self.instance.id) + # if queryset.exists(): + # raise serializers.ValidationError(_('Username already exists')) + # return value + # + # def validate_dynamic_username(self, value): + # if not value: + # return value + # queryset = self.Meta.model.objects.filter(username='@USER') + # if self.instance: + # queryset = queryset.exclude(id=self.instance.id) + # if queryset.exists(): + # raise serializers.ValidationError(_('Dynamic username already exists')) + # return value + # + # def validate_triggers(self, value): + # # Now triggers readonly, set all + # return TriggerChoice.all() + # + # def get_field_names(self, declared_fields, info): + # fields = super().get_field_names(declared_fields, info) + # excludes = [ + # 'recipients', 'is_periodic', 'interval', 'crontab', + # 'periodic_display', 'assets', 'nodes' + # ] + # fields = [f for f in fields if f not in excludes] + # fields[fields.index('accounts')] = 'username' + # return fields + + +class PushAccountUpdateAssetSerializer(ChangeSecretUpdateAssetSerializer): + class Meta: + model = PushAccountAutomation + fields = ChangeSecretUpdateAssetSerializer.Meta.fields + + +class PushAccountUpdateNodeSerializer(ChangeSecretUpdateNodeSerializer): + class Meta: + model = PushAccountAutomation + fields = ChangeSecretUpdateNodeSerializer.Meta.fields diff --git a/apps/accounts/signal_handlers.py b/apps/accounts/signal_handlers.py new file mode 100644 index 000000000..311c8d83d --- /dev/null +++ b/apps/accounts/signal_handlers.py @@ -0,0 +1,26 @@ +from django.db.models.signals import pre_save, post_save +from django.dispatch import receiver + +from assets.models import Asset +from common.decorator import on_transaction_commit +from common.utils import get_logger +from .automations.push_account.manager import PushAccountManager +from .models import Account + +logger = get_logger(__name__) + + +@receiver(pre_save, sender=Account) +def on_account_pre_create(sender, instance, **kwargs): + # 升级版本号 + instance.version += 1 + # 即使在 root 组织也不怕 + instance.org_id = instance.asset.org_id + + +@receiver(post_save, sender=Asset) +@on_transaction_commit +def on_asset_create(sender, instance, created=False, **kwargs): + if not created: + return + # PushAccountManager.trigger_by_asset_create(instance) diff --git a/apps/accounts/tasks/__init__.py b/apps/accounts/tasks/__init__.py new file mode 100644 index 000000000..055508b24 --- /dev/null +++ b/apps/accounts/tasks/__init__.py @@ -0,0 +1,5 @@ +from .backup_account import * +from .automation import * +from .push_account import * +from .verify_account import * +from .gather_accounts import * diff --git a/apps/accounts/tasks/automation.py b/apps/accounts/tasks/automation.py new file mode 100644 index 000000000..67d4c4a90 --- /dev/null +++ b/apps/accounts/tasks/automation.py @@ -0,0 +1,20 @@ +from celery import shared_task +from django.utils.translation import gettext_lazy as _ + +from orgs.utils import tmp_to_root_org, tmp_to_org +from common.utils import get_logger, get_object_or_none +from accounts.const import AutomationTypes + +logger = get_logger(__file__) + + +@shared_task(queue='ansible', verbose_name=_('Account execute automation')) +def execute_automation(pid, trigger, tp): + model = AutomationTypes.get_type_model(tp) + with tmp_to_root_org(): + instance = get_object_or_none(model, pk=pid) + if not instance: + logger.error("No automation task found: {}".format(pid)) + return + with tmp_to_org(instance.org): + instance.execute(trigger) diff --git a/apps/assets/tasks/backup.py b/apps/accounts/tasks/backup_account.py similarity index 82% rename from apps/assets/tasks/backup.py rename to apps/accounts/tasks/backup_account.py index a82a6abd1..bbcf25d1f 100644 --- a/apps/assets/tasks/backup.py +++ b/apps/accounts/tasks/backup_account.py @@ -3,9 +3,9 @@ from celery import shared_task from django.utils.translation import gettext_lazy as _ +from accounts.models import AccountBackupAutomation from common.utils import get_object_or_none, get_logger from orgs.utils import tmp_to_org, tmp_to_root_org -from assets.models import AccountBackupPlan logger = get_logger(__file__) @@ -13,7 +13,7 @@ logger = get_logger(__file__) @shared_task(verbose_name=_('Execute account backup plan')) def execute_account_backup_plan(pid, trigger): with tmp_to_root_org(): - plan = get_object_or_none(AccountBackupPlan, pk=pid) + plan = get_object_or_none(AccountBackupAutomation, pk=pid) if not plan: logger.error("No account backup route plan found: {}".format(pid)) return diff --git a/apps/accounts/tasks/common.py b/apps/accounts/tasks/common.py new file mode 100644 index 000000000..f45032939 --- /dev/null +++ b/apps/accounts/tasks/common.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +from assets.tasks.common import generate_data +from common.const.choices import Trigger + + +def automation_execute_start(task_name, tp, child_snapshot=None): + from accounts.models import AutomationExecution + data = generate_data(task_name, tp, child_snapshot) + execution = AutomationExecution.objects.create( + trigger=Trigger.manual, **data + ) + execution.start() diff --git a/apps/assets/tasks/gather_accounts.py b/apps/accounts/tasks/gather_accounts.py similarity index 63% rename from apps/assets/tasks/gather_accounts.py rename to apps/accounts/tasks/gather_accounts.py index 5e20bfe73..dbfbe981e 100644 --- a/apps/assets/tasks/gather_accounts.py +++ b/apps/accounts/tasks/gather_accounts.py @@ -3,9 +3,11 @@ from celery import shared_task from django.utils.translation import gettext_noop from django.utils.translation import gettext_lazy as _ -from orgs.utils import tmp_to_root_org, org_aware_func -from common.utils import get_logger from assets.models import Node +from common.utils import get_logger +from orgs.utils import org_aware_func +from accounts.const import AutomationTypes +from accounts.tasks.common import automation_execute_start __all__ = ['gather_asset_accounts'] logger = get_logger(__name__) @@ -13,16 +15,14 @@ logger = get_logger(__name__) @org_aware_func("nodes") def gather_asset_accounts_util(nodes, task_name): - from assets.models import GatherAccountsAutomation + from accounts.models import GatherAccountsAutomation task_name = GatherAccountsAutomation.generate_unique_name(task_name) - data = { - 'name': task_name, - 'comment': ', '.join([str(i) for i in nodes]) + child_snapshot = { + 'nodes': [str(node.id) for node in nodes], } - instance = GatherAccountsAutomation.objects.create(**data) - instance.nodes.add(*nodes) - instance.execute() + tp = AutomationTypes.verify_account + automation_execute_start(task_name, tp, child_snapshot) @shared_task(queue="ansible", verbose_name=_('Gather asset accounts')) @@ -30,6 +30,5 @@ 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) + nodes = Node.objects.filter(id__in=node_ids) gather_asset_accounts_util(nodes=nodes, task_name=task_name) diff --git a/apps/accounts/tasks/push_account.py b/apps/accounts/tasks/push_account.py new file mode 100644 index 000000000..4ae09d72b --- /dev/null +++ b/apps/accounts/tasks/push_account.py @@ -0,0 +1,43 @@ +from celery import shared_task +from django.utils.translation import gettext_noop, ugettext_lazy as _ + +from common.utils import get_logger +from orgs.utils import org_aware_func +from accounts.const import AutomationTypes +from accounts.tasks.common import automation_execute_start + +logger = get_logger(__file__) +__all__ = [ + 'push_accounts_to_assets', +] + + +def push_util(account, assets, task_name): + child_snapshot = { + 'secret': account.secret, + 'secret_type': account.secret_type, + 'accounts': [account.username], + 'assets': [str(asset.id) for asset in assets], + } + tp = AutomationTypes.push_account + automation_execute_start(task_name, tp, child_snapshot) + + +@org_aware_func("assets") +def push_accounts_to_assets_util(accounts, assets): + from accounts.models import PushAccountAutomation + + task_name = gettext_noop("Push accounts to assets") + task_name = PushAccountAutomation.generate_unique_name(task_name) + for account in accounts: + push_util(account, assets, task_name) + + +@shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) +def push_accounts_to_assets(account_ids, asset_ids): + from assets.models import Asset + from accounts.models import Account + + assets = Asset.objects.filter(id__in=asset_ids) + accounts = Account.objects.filter(id__in=account_ids) + return push_accounts_to_assets_util(accounts, assets) diff --git a/apps/assets/tasks/verify_account.py b/apps/accounts/tasks/verify_account.py similarity index 59% rename from apps/assets/tasks/verify_account.py rename to apps/accounts/tasks/verify_account.py index 4538f2b2d..0b69262f5 100644 --- a/apps/assets/tasks/verify_account.py +++ b/apps/accounts/tasks/verify_account.py @@ -3,7 +3,9 @@ from django.utils.translation import gettext_noop from django.utils.translation import ugettext as _ from common.utils import get_logger -from orgs.utils import org_aware_func, tmp_to_root_org +from accounts.tasks.common import automation_execute_start +from accounts.const import AutomationTypes +from orgs.utils import org_aware_func logger = get_logger(__name__) __all__ = [ @@ -13,26 +15,23 @@ __all__ = [ @org_aware_func("assets") def verify_accounts_connectivity_util(accounts, assets, task_name): - from assets.models import VerifyAccountAutomation + from accounts.models import VerifyAccountAutomation task_name = VerifyAccountAutomation.generate_unique_name(task_name) account_usernames = list(accounts.values_list('username', flat=True)) - data = { - 'name': task_name, + child_snapshot = { 'accounts': account_usernames, - 'comment': ', '.join([str(i) for i in assets]) + 'assets': [str(asset.id) for asset in assets], } - instance = VerifyAccountAutomation.objects.create(**data) - instance.assets.add(*assets) - instance.execute() + tp = AutomationTypes.verify_account + automation_execute_start(task_name, tp, child_snapshot) @shared_task(queue="ansible", verbose_name=_('Verify asset account availability')) def verify_accounts_connectivity(account_ids, asset_ids): - from assets.models import Asset, Account - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) - accounts = Account.objects.filter(id__in=account_ids) - + from assets.models import Asset + from accounts.models import Account + assets = Asset.objects.filter(id__in=asset_ids) + accounts = Account.objects.filter(id__in=account_ids) task_name = gettext_noop("Verify accounts connectivity") return verify_accounts_connectivity_util(accounts, assets, task_name) diff --git a/apps/accounts/tests.py b/apps/accounts/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py new file mode 100644 index 000000000..b5280d8ab --- /dev/null +++ b/apps/accounts/urls.py @@ -0,0 +1,41 @@ +# coding:utf-8 +from django.urls import path +from rest_framework_bulk.routes import BulkRouter + +from . import api + +app_name = 'accounts' + +router = BulkRouter() + +router.register(r'accounts', api.AccountViewSet, 'account') +router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') +router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') +router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') +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'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') +router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') +router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation') +router.register(r'gather-account-executions', api.GatherAccountsExecutionViewSet, 'gather-account-execution') +router.register(r'push-account-automations', api.PushAccountAutomationViewSet, 'push-account-automation') +router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'push-account-execution') +router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record') + +urlpatterns = [ + path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'), + path('account-secrets//histories/', api.AccountHistoriesSecretAPI.as_view(), name='account-secret-history'), + + path('change-secret//asset/remove/', api.ChangSecretRemoveAssetApi.as_view(), name='change-secret-remove-asset'), + path('change-secret//asset/add/', api.ChangSecretAddAssetApi.as_view(), name='change-secret-add-asset'), + path('change-secret//nodes/', api.ChangSecretNodeAddRemoveApi.as_view(), name='change-secret-add-or-remove-node'), + path('change-secret//assets/', api.ChangSecretAssetsListApi.as_view(), name='change-secret-assets'), + + path('push-account//asset/remove/', api.PushAccountRemoveAssetApi.as_view(), name='push-account-remove-asset'), + path('push-accountt//asset/add/', api.PushAccountAddAssetApi.as_view(), name='push-account-add-asset'), + path('push-account//nodes/', api.PushAccountNodeAddRemoveApi.as_view(), name='push-account-add-or-remove-node'), + path('push-account//assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'), +] + +urlpatterns += router.urls diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py new file mode 100644 index 000000000..0a64e54e0 --- /dev/null +++ b/apps/accounts/utils.py @@ -0,0 +1,54 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from accounts.const import ( + SecretType, DEFAULT_PASSWORD_RULES +) +from common.utils import gen_key_pair, random_string +from common.utils import validate_ssh_private_key, parse_ssh_private_key_str + + +class SecretGenerator: + def __init__(self, secret_strategy, secret_type, password_rules=None): + self.secret_strategy = secret_strategy + self.secret_type = secret_type + self.password_rules = password_rules + + @staticmethod + def generate_ssh_key(): + private_key, public_key = gen_key_pair() + return private_key + + def generate_password(self): + length = int(self.password_rules.get('length', DEFAULT_PASSWORD_RULES['length'])) + return random_string(length, special_char=True) + + def get_secret(self): + if self.secret_type == SecretType.SSH_KEY: + secret = self.generate_ssh_key() + elif self.secret_type == SecretType.PASSWORD: + secret = self.generate_password() + else: + raise ValueError("Secret must be set") + return secret + + +def validate_password_for_ansible(password): + """ 校验 Ansible 不支持的特殊字符 """ + # validate password contains left double curly bracket + # check password not contains `{{` + # Ansible 推送的时候不支持 + if '{{' in password: + raise serializers.ValidationError(_('Password can not contains `{{` ')) + # Ansible Windows 推送的时候不支持 + if "'" in password: + raise serializers.ValidationError(_("Password can not contains `'` ")) + 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")) + return parse_ssh_private_key_str(ssh_key, passphrase) diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py index 80163e717..44a342743 100644 --- a/apps/acls/api/command_acl.py +++ b/apps/acls/api/command_acl.py @@ -35,21 +35,5 @@ class CommandFilterACLViewSet(OrgBulkModelViewSet): 'org_id': serializer.org.id } ticket = serializer.cmd_filter_acl.create_command_review_ticket(**data) - - url_review_status = reverse( - view_name='api-tickets:super-ticket-status', kwargs={'pk': str(ticket.id)} - ) - url_ticket_detail = reverse( - view_name='api-tickets:ticket-detail', kwargs={'pk': str(ticket.id)}, - external=True, api_to_ui=True - ) - resp_data = { - 'check_review_status': {'method': 'GET', 'url': url_review_status}, - 'close_review': {'method': 'DELETE', 'url': url_review_status}, - 'ticket_detail_url': url_ticket_detail, - 'reviewers': [ - str(ticket_assignee.assignee) - for ticket_assignee in ticket.current_step.ticket_assignees.all() - ] - } - return Response(resp_data) + info = ticket.get_extra_info_of_review(user=request.user) + return info diff --git a/apps/acls/api/login_acl.py b/apps/acls/api/login_acl.py index 85e902143..806ffb343 100644 --- a/apps/acls/api/login_acl.py +++ b/apps/acls/api/login_acl.py @@ -1,4 +1,4 @@ -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet from ..models import LoginACL from .. import serializers from ..filters import LoginAclFilter diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 386f4480c..a593f0c27 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView): return response_data def _get_response_data_of_need_review(self, acl) -> dict: - ticket = LoginAssetACL.create_login_asset_confirm_ticket( + ticket = LoginAssetACL.create_login_asset_review_ticket( user=self.serializer.user, asset=self.serializer.asset, account_username=self.serializer.validated_data.get('account_username'), diff --git a/apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py b/apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py new file mode 100644 index 000000000..b657906f1 --- /dev/null +++ b/apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('acls', '0009_auto_20221220_1956'), + ] + + operations = [ + migrations.AlterField( + model_name='commandfilteracl', + name='command_groups', + field=models.ManyToManyField(to='acls.CommandGroup', verbose_name='Command group'), + ), + ] diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 256241361..7ba48b3c6 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -85,6 +85,9 @@ class BaseACL(JMSBaseModel): ordering = ('priority', 'name') abstract = True + def is_action(self, action): + return self.action == action + class UserAssetAccountBaseACL(BaseACL, OrgModelMixin): # username_group diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index b02bc09be..9029142f2 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -93,7 +93,7 @@ class CommandGroup(JMSOrgBaseModel): class CommandFilterACL(UserAssetAccountBaseACL): - command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Commands')) + command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Command group')) class Meta(UserAssetAccountBaseACL.Meta): abstract = False diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index bdaf9b60c..609491e8d 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -14,7 +14,7 @@ class LoginAssetACL(UserAssetAccountBaseACL): return self.name @classmethod - def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id): + def create_login_asset_review_ticket(cls, user, asset, account_username, assignees, org_id): from tickets.const import TicketType from tickets.models import ApplyLoginAssetTicket title = _('Login asset confirm') + ' ({})'.format(user) diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py index fdf6f8a23..24b6867e7 100644 --- a/apps/acls/serializers/base.py +++ b/apps/acls/serializers/base.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from acls.models.base import ActionChoices -from common.drf.fields import LabeledChoiceField, ObjectRelatedField +from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from orgs.models import Organization from users.models import User @@ -55,10 +55,28 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): users = ACLUsersSerializer(label=_('User')) assets = ACLAssestsSerializer(label=_('Asset')) accounts = ACLAccountsSerializer(label=_('Account')) + users_username_group = serializers.ListField( + source='users.username_group', read_only=True, child=serializers.CharField(), + label=_('User (username)') + ) + assets_name_group = serializers.ListField( + source='assets.name_group', read_only=True, child=serializers.CharField(), + label=_('Asset (name)') + ) + assets_address_group = serializers.ListField( + source='assets.address_group', read_only=True, child=serializers.CharField(), + label=_('Asset (address)') + ) + accounts_username_group = serializers.ListField( + source='accounts.username_group', read_only=True, child=serializers.CharField(), + label=_('Account (username)') + ) reviewers = ObjectRelatedField( queryset=User.objects, many=True, required=False, label=_('Reviewers') ) - reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") + reviewers_amount = serializers.IntegerField( + read_only=True, source="reviewers.count", label=_('Reviewers amount') + ) action = LabeledChoiceField( choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action") ) @@ -66,6 +84,8 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): class Meta: fields_mini = ["id", "name"] fields_small = fields_mini + [ + 'users_username_group', 'assets_address_group', 'assets_name_group', + 'accounts_username_group', "users", "accounts", "assets", "is_active", "date_created", "date_updated", "priority", "action", "comment", "created_by", "org_id", @@ -73,7 +93,6 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): fields_m2m = ["reviewers", "reviewers_amount"] fields = fields_small + fields_m2m extra_kwargs = { - "reviewers": {"allow_null": False, "required": True}, "priority": {"default": 50}, "is_active": {"default": True}, } diff --git a/apps/acls/serializers/command_acl.py b/apps/acls/serializers/command_acl.py index 07f2e39a7..f4729ffd5 100644 --- a/apps/acls/serializers/command_acl.py +++ b/apps/acls/serializers/command_acl.py @@ -1,11 +1,10 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers - from terminal.models import Session from acls.models import CommandGroup, CommandFilterACL from common.utils import lazyproperty, get_object_or_none -from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from orgs.utils import tmp_to_root_org from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer @@ -28,10 +27,13 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) command_groups = ObjectRelatedField( queryset=CommandGroup.objects, many=True, required=False, label=_('Command group') ) + command_groups_amount = serializers.IntegerField( + source='command_groups.count', read_only=True, label=_('Command group amount') + ) class Meta(BaseSerializer.Meta): model = CommandFilterACL - fields = BaseSerializer.Meta.fields + ['command_groups'] + fields = BaseSerializer.Meta.fields + ['command_groups', 'command_groups_amount'] class CommandReviewSerializer(serializers.Serializer): diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index db89445c8..99b1f1077 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -1,8 +1,8 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.drf.fields import ObjectRelatedField, LabeledChoiceField -from common.drf.serializers import BulkModelSerializer, MethodSerializer +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers import BulkModelSerializer, MethodSerializer from jumpserver.utils import has_valid_xpack_license from users.models import User from .rules import RuleSerializer @@ -24,9 +24,9 @@ class LoginACLSerializer(BulkModelSerializer): ) action = LabeledChoiceField(choices=LoginACL.ActionChoices.choices) reviewers_amount = serializers.IntegerField( - read_only=True, source="reviewers.count" + read_only=True, source="reviewers.count", label=_("Reviewers amount") ) - rules = MethodSerializer() + rules = MethodSerializer(label=_('Rule')) class Meta: model = LoginACL @@ -34,15 +34,14 @@ class LoginACLSerializer(BulkModelSerializer): fields_small = fields_mini + [ "priority", "user", "rules", "action", "is_active", "date_created", "date_updated", - "reviewers_amount", "comment", "created_by", + "comment", "created_by", ] fields_fk = ["user"] - fields_m2m = ["reviewers"] + fields_m2m = ["reviewers", "reviewers_amount"] fields = fields_small + fields_fk + fields_m2m extra_kwargs = { "priority": {"default": 50}, "is_active": {"default": True}, - "reviewers": {"allow_null": False, "required": True}, } def __init__(self, *args, **kwargs): diff --git a/apps/applications/migrations/0026_auto_20220817_1716.py b/apps/applications/migrations/0026_auto_20220817_1716.py index 5fbf9c1d6..bb9df0808 100644 --- a/apps/applications/migrations/0026_auto_20220817_1716.py +++ b/apps/applications/migrations/0026_auto_20220817_1716.py @@ -42,7 +42,7 @@ class Migration(migrations.Migration): ('applications', '0025_auto_20220817_1346'), ('perms', '0031_auto_20220816_1600'), ('ops', '0022_auto_20220817_1346'), - ('assets', '0105_auto_20220817_1544'), + ('assets', '0100_auto_20220711_1413'), ('tickets', '0020_auto_20220817_1346'), ] diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 67a935f84..186ab10b7 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,6 +1,4 @@ -from .account import * from .asset import * -from .automations import * from .category import * from .domain import * from .favorite_asset import * diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 9ba11a872..08c877bcb 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- # - import django_filters +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action from rest_framework.response import Response +from accounts.tasks import push_accounts_to_assets, verify_accounts_connectivity from assets import serializers from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend from assets.models import Asset, Gateway from assets.tasks import ( - push_accounts_to_assets, test_assets_connectivity_manual, - update_assets_hardware_info_manual, verify_accounts_connectivity, + test_assets_connectivity_manual, + update_assets_hardware_info_manual ) +from common.api import SuggestionMixin from common.drf.filters import BaseFilterSet -from common.mixins.api import SuggestionMixin from common.utils import get_logger from orgs.mixins import generics from orgs.mixins.api import OrgBulkModelViewSet @@ -21,10 +23,8 @@ from ..mixin import NodeFilterMixin logger = get_logger(__file__) __all__ = [ - "AssetViewSet", - "AssetTaskCreateApi", - "AssetsTaskCreateApi", - 'AssetFilterSet' + "AssetViewSet", "AssetTaskCreateApi", + "AssetsTaskCreateApi", 'AssetFilterSet' ] @@ -32,11 +32,12 @@ class AssetFilterSet(BaseFilterSet): type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact") category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact") platform = django_filters.CharFilter(method='filter_platform') + labels = django_filters.CharFilter(method='filter_labels') class Meta: model = Asset fields = [ - "id", "name", "address", "is_active", + "id", "name", "address", "is_active", "labels", "type", "category", "platform" ] @@ -47,12 +48,20 @@ class AssetFilterSet(BaseFilterSet): else: return queryset.filter(platform__name=value) + @staticmethod + def filter_labels(queryset, name, value): + if ':' in value: + n, v = value.split(':', 1) + queryset = queryset.filter(labels__name=n, labels__value=v) + else: + queryset = queryset.filter(Q(labels__name=value) | Q(labels__value=value)) + return queryset + class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ - model = Asset filterset_class = AssetFilterSet search_fields = ("name", "address") @@ -60,7 +69,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ordering = ("name",) serializer_classes = ( ("default", serializers.AssetSerializer), - ("retrieve", serializers.AssetDetailSerializer), ("platform", serializers.PlatformSerializer), ("suggestion", serializers.MiniAssetSerializer), ("gateways", serializers.GatewaySerializer), @@ -72,10 +80,18 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ) extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] + def get_serializer_class(self): + cls = super().get_serializer_class() + if self.action == "retrieve": + name = cls.__name__.replace("Serializer", "DetailSerializer") + retrieve_cls = type(name, (serializers.DetailMixin, cls), {}) + return retrieve_cls + return cls + @action(methods=["GET"], detail=True, url_path="platform") def platform(self, *args, **kwargs): - asset = self.get_object() - serializer = self.get_serializer(asset.platform) + asset = super().get_object() + serializer = super().get_serializer(instance=asset.platform) return Response(serializer.data) @action(methods=["GET"], detail=True, url_path="gateways") @@ -120,14 +136,14 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): return super().create(request, *args, **kwargs) def check_permissions(self, request): - action = request.data.get("action") action_perm_require = { "refresh": "assets.refresh_assethardwareinfo", - "push_account": "assets.push_assetsystemuser", + "push_account": "accounts.add_pushaccountexecution", "test": "assets.test_assetconnectivity", - "test_account": "assets.test_assetconnectivity", + "test_account": "assets.test_account", } - perm_required = action_perm_require.get(action) + _action = request.data.get("action") + perm_required = action_perm_require.get(_action) has = self.request.user.has_perm(perm_required) if not has: @@ -136,7 +152,7 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): @staticmethod def perform_asset_task(serializer): data = serializer.validated_data - if data["action"] not in ["push_system_user", "test_system_user"]: + if data["action"] not in ["push_account", "test_account"]: return asset = data["asset"] @@ -166,11 +182,11 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): serializer_class = serializers.AssetsTaskSerializer def check_permissions(self, request): - action = request.data.get("action") action_perm_require = { "refresh": "assets.refresh_assethardwareinfo", } - perm_required = action_perm_require.get(action) + _action = request.data.get("action") + perm_required = action_perm_require.get(_action) has = self.request.user.has_perm(perm_required) if not has: self.permission_denied(request) diff --git a/apps/assets/api/automations/change_secret.py b/apps/assets/api/automations/change_secret.py deleted file mode 100644 index 550a3aad0..000000000 --- a/apps/assets/api/automations/change_secret.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import mixins - -from assets import serializers -from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution -from common.utils import get_object_or_none -from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet -from .base import AutomationExecutionViewSet - -__all__ = [ - 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', - 'ChangSecretExecutionViewSet' -] - - -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('-date_started') - return queryset - - -class ChangSecretExecutionViewSet(AutomationExecutionViewSet): - rbac_perms = ( - ("list", "assets.view_changesecretexecution"), - ("retrieve", "assets.view_changesecretexecution"), - ("create", "assets.add_changesecretexecution"), - ) diff --git a/apps/assets/api/category.py b/apps/assets/api/category.py index ae44ddc9b..17deba4d3 100644 --- a/apps/assets/api/category.py +++ b/apps/assets/api/category.py @@ -2,7 +2,7 @@ from rest_framework.mixins import ListModelMixin from rest_framework.decorators import action from rest_framework.response import Response -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from assets.serializers import CategorySerializer, TypeSerializer from assets.const import AllTypes diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 954f4842c..280a8c40c 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -1,13 +1,14 @@ # ~*~ coding: utf-8 ~*~ -from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext as _ -from rest_framework.views import APIView, Response +from django.views.generic.detail import SingleObjectMixin from rest_framework.serializers import ValidationError +from rest_framework.views import APIView, Response from common.utils import get_logger +from assets.tasks import test_assets_connectivity_manual from orgs.mixins.api import OrgBulkModelViewSet -from ..models import Domain, Gateway from .. import serializers +from ..models import Domain, Gateway logger = get_logger(__file__) __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] @@ -40,7 +41,7 @@ class GatewayViewSet(OrgBulkModelViewSet): class GatewayTestConnectionApi(SingleObjectMixin, APIView): rbac_perms = { - 'POST': 'assets.test_gateway' + 'POST': 'assets.test_assetconnectivity' } def get_queryset(self): @@ -54,8 +55,5 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView): local_port = int(local_port) except ValueError: raise ValidationError({'port': _('Number required')}) - ok, e = gateway.test_connective(local_port=local_port) - if ok: - return Response("ok") - else: - return Response({"error": e}, status=400) + task = test_assets_connectivity_manual.delay([gateway.id], local_port) + return Response({'task': task.id}) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index b45b2170c..bb81a3b45 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,24 +1,24 @@ # ~*~ coding: utf-8 ~*~ -from collections import namedtuple, defaultdict from functools import partial +from collections import namedtuple, defaultdict from django.db.models.signals import m2m_changed from django.utils.translation import ugettext_lazy as _ from rest_framework import status +from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 -from rest_framework.response import Response from rest_framework.serializers import ValidationError from assets.models import Asset from common.const.http import POST -from common.const.signals import PRE_REMOVE, POST_REMOVE -from common.exceptions import SomeoneIsDoingThis -from common.mixins.api import SuggestionMixin from common.utils import get_logger +from common.api import SuggestionMixin +from common.exceptions import SomeoneIsDoingThis +from common.const.signals import PRE_REMOVE, POST_REMOVE from orgs.mixins import generics -from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org +from orgs.mixins.api import OrgBulkModelViewSet from .. import serializers from ..models import Node from ..tasks import ( @@ -208,8 +208,9 @@ class NodeTaskCreateApi(generics.CreateAPIView): task = self.refresh_nodes_cache() self.set_serializer_data(serializer, task) return + if action == "refresh": - task = update_node_assets_hardware_info_manual.delay(node) + task = update_node_assets_hardware_info_manual.delay(node.id) else: - task = test_node_assets_connectivity_manual.delay(node) + task = test_node_assets_connectivity_manual.delay(node.id) self.set_serializer_data(serializer, task) diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py index dbfcc2e4c..2d68b2350 100644 --- a/apps/assets/api/platform.py +++ b/apps/assets/api/platform.py @@ -1,6 +1,6 @@ from jumpserver.utils import has_valid_xpack_license -from common.drf.api import JMSModelViewSet -from common.drf.serializers import GroupedChoiceSerializer +from common.api import JMSModelViewSet +from common.serializers import GroupedChoiceSerializer from assets.models import Platform from assets.const import AllTypes from assets.serializers import PlatformSerializer diff --git a/apps/assets/api/tree.py b/apps/assets/api/tree.py index 2d07d88b0..dcbc191d8 100644 --- a/apps/assets/api/tree.py +++ b/apps/assets/api/tree.py @@ -1,5 +1,6 @@ # ~*~ coding: utf-8 ~*~ +from django.db.models import Q from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -107,25 +108,38 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): model = Node def filter_queryset(self, queryset): + """ queryset is Node queryset """ if not self.request.GET.get('search'): return queryset queryset = super().filter_queryset(queryset) queryset = self.model.get_ancestor_queryset(queryset) return queryset - def list(self, request, *args, **kwargs): - nodes = self.filter_queryset(self.get_queryset()).order_by('value') - nodes = self.serialize_nodes(nodes, with_asset_amount=True) - assets = self.get_assets_as_node() - data = [*nodes, *assets] - return Response(data=data) - - def get_assets_as_node(self): + def get_queryset_for_assets(self): + query_all = self.request.query_params.get("all", "0") == "all" include_assets = self.request.query_params.get('assets', '0') == '1' if not self.instance or not include_assets: return [] - assets = self.instance.get_assets_for_tree() - return self.serialize_assets(assets, self.instance.key) + if query_all: + assets = self.instance.get_all_assets_for_tree() + else: + assets = self.instance.get_assets_for_tree() + return assets + + def filter_queryset_for_assets(self, assets): + search = self.request.query_params.get('search') + if search: + q = Q(name__icontains=search) | Q(address__icontains=search) + assets = assets.filter(q) + return assets + + def list(self, request, *args, **kwargs): + nodes = self.filter_queryset(self.get_queryset()).order_by('value') + nodes = self.serialize_nodes(nodes, with_asset_amount=True) + assets = self.filter_queryset_for_assets(self.get_queryset_for_assets()) + assets = self.serialize_assets(assets, self.instance.key) + data = [*nodes, *assets] + return Response(data=data) class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): @@ -145,9 +159,11 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): def list(self, request, *args, **kwargs): include_asset = self.request.query_params.get('assets', '0') == '1' + # 资源数量统计可选项 (asset, account) + count_resource = self.request.query_params.get('count_resource', 'asset') if include_asset and self.request.query_params.get('key'): nodes = self.get_assets() else: - nodes = AllTypes.to_tree_nodes(include_asset) + nodes = AllTypes.to_tree_nodes(include_asset, count_resource=count_resource) return Response(data=nodes) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index b2f76b43d..50a5160e2 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -1,7 +1,6 @@ import os import shutil from collections import defaultdict -from copy import deepcopy from hashlib import md5 from socket import gethostname @@ -11,7 +10,6 @@ from django.utils import timezone from django.utils.translation import gettext as _ from assets.automations.methods import platform_automation_methods -from assets.const import SecretType from common.utils import get_logger, lazyproperty from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback @@ -19,50 +17,6 @@ from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback logger = get_logger(__name__) -class PushOrVerifyHostCallbackMixin: - execution: callable - host_account_mapper: dict - ignore_account: bool - need_privilege_account: bool - generate_public_key: callable - generate_private_key_path: callable - - def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): - host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) - if host.get('error'): - return host - - accounts = asset.accounts.all() - if self.need_privilege_account and accounts.count() > 1 and account: - accounts = accounts.exclude(id=account.id) - - if '*' not in self.execution.snapshot['accounts']: - accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) - - inventory_hosts = [] - for account in accounts: - h = deepcopy(host) - h['name'] += '_' + account.username - self.host_account_mapper[h['name']] = account - secret = account.secret - - private_key_path = None - if account.secret_type == SecretType.SSH_KEY: - private_key_path = self.generate_private_key_path(secret, path_dir) - secret = self.generate_public_key(secret) - - h['secret_type'] = account.secret_type - h['account'] = { - 'name': account.name, - 'username': account.username, - 'secret_type': account.secret_type, - 'secret': secret, - 'private_key_path': private_key_path - } - inventory_hosts.append(h) - return inventory_hosts - - class PlaybookCallback(DefaultCallback): def playbook_on_stats(self, event_data, **kwargs): super().playbook_on_stats(event_data, **kwargs) @@ -74,10 +28,9 @@ class BasePlaybookManager: def __init__(self, execution): self.execution = execution - self.automation = execution.automation self.method_id_meta_mapper = { method['id']: method - for method in platform_automation_methods + for method in self.platform_automation_methods if method['method'] == self.__class__.method_type() } # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 @@ -86,17 +39,22 @@ class BasePlaybookManager: self.method_hosts_mapper = defaultdict(list) self.playbooks = [] + @property + def platform_automation_methods(self): + return platform_automation_methods + @classmethod def method_type(cls): raise NotImplementedError def get_assets_group_by_platform(self): - return self.automation.all_assets_group_by_platform() + return self.execution.all_assets_group_by_platform() @lazyproperty def runtime_dir(self): ansible_dir = settings.ANSIBLE_DIR - dir_name = '{}_{}'.format(self.automation.name.replace(' ', '_'), self.execution.id) + task_name = self.execution.snapshot['name'] + dir_name = '{}_{}'.format(task_name.replace(' ', '_'), self.execution.id) path = os.path.join( ansible_dir, 'automations', self.execution.snapshot['type'], dir_name, timezone.now().strftime('%Y%m%d_%H%M%S') @@ -205,9 +163,7 @@ class BasePlaybookManager: print("Runner failed: {} {}".format(e, self)) def before_runner_start(self, runner): - print("Start run task: ") - print(" inventory: {}".format(runner.inventory)) - print(" playbook: {}".format(runner.playbook)) + pass def run(self, *args, **kwargs): runners = self.get_runners() diff --git a/apps/assets/automations/endpoint.py b/apps/assets/automations/endpoint.py index 2548e9184..99feebc49 100644 --- a/apps/assets/automations/endpoint.py +++ b/apps/assets/automations/endpoint.py @@ -1,23 +1,14 @@ -from .change_secret.manager import ChangeSecretManager -from .gather_facts.manager import GatherFactsManager -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 .ping_gateway.manager import PingGatewayManager +from .gather_facts.manager import GatherFactsManager from ..const import AutomationTypes class ExecutionManager: manager_type_mapper = { AutomationTypes.ping: PingManager, - AutomationTypes.push_account: PushAccountManager, + AutomationTypes.ping_gateway: PingGatewayManager, AutomationTypes.gather_facts: GatherFactsManager, - AutomationTypes.change_secret: ChangeSecretManager, - AutomationTypes.verify_account: VerifyAccountManager, - AutomationTypes.gather_accounts: GatherAccountsManager, - # TODO 后期迁移到自动化策略中 - 'backup_account': AccountBackupManager, } def __init__(self, execution): diff --git a/apps/assets/automations/gather_facts/database/mysql/manifest.yml b/apps/assets/automations/gather_facts/database/mysql/manifest.yml index 33109b29b..7149e96e9 100644 --- a/apps/assets/automations/gather_facts/database/mysql/manifest.yml +++ b/apps/assets/automations/gather_facts/database/mysql/manifest.yml @@ -3,4 +3,5 @@ name: Gather facts from MySQL category: database type: - mysql + - mariadb method: gather_facts diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py index 811d0c77d..86fcb775c 100644 --- a/apps/assets/automations/methods.py +++ b/apps/assets/automations/methods.py @@ -3,8 +3,6 @@ import yaml import json from functools import partial -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - def check_platform_method(manifest, manifest_path): required_keys = ['category', 'method', 'name', 'id', 'type'] @@ -19,13 +17,13 @@ def check_platform_method(manifest, manifest_path): def check_platform_methods(methods): ids = [m['id'] for m in methods] for i, _id in enumerate(ids): - if _id in ids[i+1:]: + if _id in ids[i + 1:]: raise ValueError("Duplicate id: {}".format(_id)) -def get_platform_automation_methods(): +def get_platform_automation_methods(path): methods = [] - for root, dirs, files in os.walk(BASE_DIR, topdown=False): + for root, dirs, files in os.walk(path, topdown=False): for name in files: path = os.path.join(root, name) if not path.endswith('manifest.yml'): @@ -48,8 +46,8 @@ def filter_key(manifest, attr, value): return value in manifest_value or 'all' in manifest_value -def filter_platform_methods(category, tp, method=None): - methods = platform_automation_methods +def filter_platform_methods(category, tp, method=None, methods=None): + methods = platform_automation_methods if methods is None else methods if category: methods = filter(partial(filter_key, attr='category', value=category), methods) if tp: @@ -59,8 +57,8 @@ def filter_platform_methods(category, tp, method=None): return methods -platform_automation_methods = get_platform_automation_methods() - +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +platform_automation_methods = get_platform_automation_methods(BASE_DIR) if __name__ == '__main__': print(json.dumps(platform_automation_methods, indent=4)) diff --git a/apps/assets/automations/ping/database/mysql/manifest.yml b/apps/assets/automations/ping/database/mysql/manifest.yml index aded00b1f..044907c6a 100644 --- a/apps/assets/automations/ping/database/mysql/manifest.yml +++ b/apps/assets/automations/ping/database/mysql/manifest.yml @@ -3,4 +3,5 @@ name: Ping MySQL category: database type: - mysql + - mariadb method: ping diff --git a/apps/assets/automations/ping_gateway/__init__.py b/apps/assets/automations/ping_gateway/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/ping_gateway/manager.py b/apps/assets/automations/ping_gateway/manager.py new file mode 100644 index 000000000..7e40cda4e --- /dev/null +++ b/apps/assets/automations/ping_gateway/manager.py @@ -0,0 +1,123 @@ +import socket +import paramiko + +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger +from assets.const import AutomationTypes, Connectivity +from assets.models import Gateway + +logger = get_logger(__name__) + + +class PingGatewayManager: + + def __init__(self, execution): + self.execution = execution + + @classmethod + def method_type(cls): + return AutomationTypes.ping_gateway + + def execute_task(self, gateway, account): + from accounts.models import Account + local_port = self.execution.snapshot.get('local_port') + local_port = gateway.port if local_port is None else local_port + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + proxy = paramiko.SSHClient() + proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + if not isinstance(account, Account): + err = _('No account') + return False, err + + logger.debug('Test account: {}'.format(account)) + try: + proxy.connect( + gateway.address, + port=gateway.port, + username=account.username, + password=account.secret, + pkey=account.private_key_obj + ) + except( + paramiko.AuthenticationException, + paramiko.BadAuthenticationType, + paramiko.SSHException, + paramiko.ChannelException, + paramiko.ssh_exception.NoValidConnectionsError, + socket.gaierror + ) as e: + err = str(e) + if err.startswith('[Errno None] Unable to connect to port'): + err = _('Unable to connect to port {port} on {address}') + err = err.format(port=gateway.port, address=gateway.address) + elif err == 'Authentication failed.': + err = _('Authentication failed') + elif err == 'Connect failed': + err = _('Connect failed') + return False, err + + try: + sock = proxy.get_transport().open_channel( + 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0) + ) + client.connect( + '127.0.0.1', + sock=sock, + timeout=5, + port=local_port, + username=account.username, + password=account.secret, + key_filename=account.private_key_path, + ) + except ( + paramiko.SSHException, + paramiko.ssh_exception.SSHException, + paramiko.ChannelException, + paramiko.AuthenticationException, + TimeoutError + ) as e: + + err = getattr(e, 'text', str(e)) + if err == 'Connect failed': + err = _('Connect failed') + return False, err + finally: + client.close() + return True, None + + @staticmethod + def on_host_success(gateway, account): + logger.info('\033[32m {}\033[0m\n'.format(gateway)) + gateway.set_connectivity(Connectivity.OK) + if not account: + return + account.set_connectivity(Connectivity.OK) + + @staticmethod + def on_host_error(gateway, account, error): + logger.info('\033[31m {} 原因: {} \033[0m\n'.format(gateway, error)) + gateway.set_connectivity(Connectivity.FAILED) + if not account: + return + account.set_connectivity(Connectivity.FAILED) + + def run(self): + asset_ids = self.execution.snapshot['assets'] + gateways = Gateway.objects.filter(id__in=asset_ids) + self.execution.date_start = timezone.now() + logger.info(">>> 开始执行测试网关可连接性任务") + for gateway in gateways: + account = gateway.select_account + ok, e = self.execute_task(gateway, account) + if ok: + self.on_host_success(gateway, account) + else: + self.on_host_error(gateway, account, e) + print('\n') + self.execution.status = 'success' + self.execution.date_finished = timezone.now() + self.execution.save() diff --git a/apps/assets/automations/push_account/manager.py b/apps/assets/automations/push_account/manager.py deleted file mode 100644 index f849f3e6e..000000000 --- a/apps/assets/automations/push_account/manager.py +++ /dev/null @@ -1,17 +0,0 @@ -from common.utils import get_logger -from assets.const import AutomationTypes -from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin - -logger = get_logger(__name__) - - -class PushAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): - need_privilege_account = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.host_account_mapper = {} - - @classmethod - def method_type(cls): - return AutomationTypes.push_account diff --git a/apps/assets/const/__init__.py b/apps/assets/const/__init__.py index bc30f388d..9e3f2cbb1 100644 --- a/apps/assets/const/__init__.py +++ b/apps/assets/const/__init__.py @@ -1,7 +1,6 @@ -from .base import * -from .host import * -from .types import * -from .account import * -from .protocol import * -from .category import * from .automation import * +from .base import * +from .category import * +from .host import * +from .protocol import * +from .types import * diff --git a/apps/assets/const/automation.py b/apps/assets/const/automation.py index 72cd5704e..a811cb50b 100644 --- a/apps/assets/const/automation.py +++ b/apps/assets/const/automation.py @@ -1,47 +1,25 @@ from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ -string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' -DEFAULT_PASSWORD_LENGTH = 30 -DEFAULT_PASSWORD_RULES = { - 'length': DEFAULT_PASSWORD_LENGTH, - 'symbol_set': string_punctuation -} + +class Connectivity(TextChoices): + UNKNOWN = 'unknown', _('Unknown') + OK = 'ok', _('Ok') + FAILED = 'failed', _('Failed') class AutomationTypes(TextChoices): ping = 'ping', _('Ping') + ping_gateway = 'ping_gateway', _('Ping gateway') gather_facts = 'gather_facts', _('Gather facts') - push_account = 'push_account', _('Push account') - change_secret = 'change_secret', _('Change secret') - 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') - random_one = 'random_one', _('All assets use the same random password') - random_all = 'random_all', _('All assets use different random password') - - -class SSHKeyStrategy(TextChoices): - add = 'add', _('Append SSH KEY') - set = 'set', _('Empty and append SSH KEY') - set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ') diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py index 9e76946f3..e14867d69 100644 --- a/apps/assets/const/category.py +++ b/apps/assets/const/category.py @@ -14,6 +14,12 @@ class Category(ChoicesMixin, models.TextChoices): CLOUD = 'cloud', _("Cloud service") WEB = 'web', _("Web") + @classmethod + def filter_choices(cls, category): + _category = getattr(cls, category.upper(), None) + choices = [(_category.value, _category.label)] if _category else cls.choices + return choices + diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py index 22240cc77..be2637ddf 100644 --- a/apps/assets/const/cloud.py +++ b/apps/assets/const/cloud.py @@ -25,6 +25,7 @@ class CloudTypes(BaseType): 'gather_facts_enabled': False, 'verify_account_enabled': False, 'change_secret_enabled': False, + 'push_account_enabled': False, 'gather_accounts_enabled': False, } } diff --git a/apps/assets/const/database.py b/apps/assets/const/database.py index 53c06bff0..05a0afc64 100644 --- a/apps/assets/const/database.py +++ b/apps/assets/const/database.py @@ -29,11 +29,19 @@ class DatabaseTypes(BaseType): 'ansible_config': { 'ansible_connection': 'local', }, + 'ping_enabled': True, 'gather_facts_enabled': True, 'gather_accounts_enabled': True, 'verify_account_enabled': True, 'change_secret_enabled': True, - } + 'push_account_enabled': True, + }, + cls.REDIS: { + 'push_account_enabled': False, + }, + cls.CLICKHOUSE: { + 'push_account_enabled': False, + }, } return constrains diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py index cbd9d7b27..a1913994c 100644 --- a/apps/assets/const/device.py +++ b/apps/assets/const/device.py @@ -40,6 +40,7 @@ class DeviceTypes(BaseType): 'gather_accounts_enabled': False, 'verify_account_enabled': False, 'change_secret_enabled': False, + 'push_account_enabled': False } } diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index 91c6b51be..295337b51 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -54,6 +54,7 @@ class HostTypes(BaseType): 'gather_accounts_enabled': True, 'verify_account_enabled': True, 'change_secret_enabled': True, + 'push_account_enabled': True }, cls.WINDOWS: { 'ansible_config': { @@ -74,9 +75,7 @@ class HostTypes(BaseType): {'name': 'Unix'}, {'name': 'macOS'}, {'name': 'BSD'}, - {'name': 'AIX', 'automation': { - 'change_secret_method': 'push_secret_aix' - }} + {'name': 'AIX'}, ], cls.WINDOWS: [ {'name': 'Windows'}, diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index 1bf61a583..dcdb03ed3 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -27,6 +27,11 @@ class AllTypes(ChoicesMixin): choices.extend(tp.choices) return choices + @classmethod + def filter_choices(cls, category): + choices = dict(cls.category_types()).get(category, cls).choices + return choices() if callable(choices) else choices + @classmethod def get_constraints(cls, category, tp): types_cls = dict(cls.category_types()).get(category) @@ -44,16 +49,25 @@ class AllTypes(ChoicesMixin): return None return constraints.get('protocols')[0]['name'] + @classmethod + def get_automation_methods(cls): + from assets.automations import platform_automation_methods as asset_methods + from accounts.automations import platform_automation_methods as account_methods + return asset_methods + account_methods + @classmethod def set_automation_methods(cls, category, tp, constraints): from assets.automations import filter_platform_methods automation = constraints.get('automation', {}) automation_methods = {} + platform_automation_methods = cls.get_automation_methods() for item, enabled in automation.items(): if not enabled: continue item_name = item.replace('_enabled', '') - methods = filter_platform_methods(category, tp, item_name) + methods = filter_platform_methods( + category, tp, item_name, methods=platform_automation_methods + ) methods = [{'name': m['name'], 'id': m['id']} for m in methods] automation_methods[item_name + '_methods'] = methods automation.update(automation_methods) @@ -162,11 +176,16 @@ class AllTypes(ChoicesMixin): return node @classmethod - def to_tree_nodes(cls, include_asset): + def to_tree_nodes(cls, include_asset, count_resource='asset'): + from accounts.models import Account from ..models import Asset, Platform - asset_platforms = Asset.objects.all().values_list('platform_id', flat=True) + if count_resource == 'account': + resource_platforms = Account.objects.all().values_list('asset__platform_id', flat=True) + else: + resource_platforms = Asset.objects.all().values_list('platform_id', flat=True) + platform_count = defaultdict(int) - for platform_id in asset_platforms: + for platform_id in resource_platforms: platform_count[platform_id] += 1 category_type_mapper = defaultdict(int) diff --git a/apps/assets/filters.py b/apps/assets/filters.py index c60d492e3..a9cb5a614 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -8,7 +8,7 @@ from rest_framework.compat import coreapi, coreschema from assets.utils import get_node_from_request, is_query_node_all_assets from common.drf.filters import BaseFilterSet -from .models import Account, Label, Node +from .models import Label, Node class AssetByNodeFilterBackend(filters.BaseFilterBackend): @@ -145,39 +145,3 @@ class IpInFilterBackend(filters.BaseFilterBackend): ) ) ] - - -class AccountFilterSet(BaseFilterSet): - ip = drf_filters.CharFilter(field_name='address', lookup_expr='exact') - hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact') - username = drf_filters.CharFilter(field_name="username", lookup_expr='exact') - address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact') - asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact') - assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') - nodes = drf_filters.CharFilter(method='filter_nodes') - has_secret = drf_filters.BooleanFilter(method='filter_has_secret') - - @staticmethod - def filter_has_secret(queryset, name, has_secret): - q = Q(secret__isnull=True) | Q(secret='') - if has_secret: - return queryset.exclude(q) - else: - return queryset.filter(q) - - @staticmethod - def filter_nodes(queryset, name, value): - nodes = Node.objects.filter(id=value) - if not nodes: - return queryset - - node_qs = Node.objects.none() - for node in nodes: - node_qs |= node.get_all_children(with_self=True) - node_ids = list(node_qs.values_list('id', flat=True)) - queryset = queryset.filter(asset__nodes__in=node_ids) - return queryset - - class Meta: - model = Account - fields = ['id'] diff --git a/apps/assets/migrations/0093_auto_20220403_1627.py b/apps/assets/migrations/0093_auto_20220403_1627.py index 752b3890a..ee536497e 100644 --- a/apps/assets/migrations/0093_auto_20220403_1627.py +++ b/apps/assets/migrations/0093_auto_20220403_1627.py @@ -113,6 +113,11 @@ class Migration(migrations.Migration): models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), ('db_name', models.CharField(blank=True, max_length=1024, verbose_name='Database')), + ('allow_invalid_cert', models.BooleanField(default=False, verbose_name='Allow invalid cert')), + ('ca_cert', models.TextField(blank=True, verbose_name='CA cert')), + ('client_cert', models.TextField(blank=True, verbose_name='Client cert')), + ('client_key', models.TextField(blank=True, verbose_name='Client key'),), + ('use_ssl', models.BooleanField(default=False, verbose_name='Use SSL'),), ], options={ 'verbose_name': 'Database', diff --git a/apps/assets/migrations/0095_auto_20220407_1726.py b/apps/assets/migrations/0095_auto_20220407_1726.py index 9d9baf42c..fa0798a20 100644 --- a/apps/assets/migrations/0095_auto_20220407_1726.py +++ b/apps/assets/migrations/0095_auto_20220407_1726.py @@ -37,11 +37,6 @@ class Migration(migrations.Migration): name='domain_enabled', field=models.BooleanField(default=True, verbose_name='Domain enabled'), ), - migrations.AddField( - model_name='platform', - name='protocols_enabled', - field=models.BooleanField(default=True, verbose_name='Protocols enabled'), - ), migrations.AddField( model_name='platform', name='su_enabled', diff --git a/apps/assets/migrations/0096_auto_20220426_1550.py b/apps/assets/migrations/0096_auto_20220426_1550.py index 30c4b76be..274a22331 100644 --- a/apps/assets/migrations/0096_auto_20220426_1550.py +++ b/apps/assets/migrations/0096_auto_20220426_1550.py @@ -33,9 +33,12 @@ class Migration(migrations.Migration): ('gather_facts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')), ('gather_facts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')), - ('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change password enabled')), + ('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change secret enabled')), ('change_secret_method', - models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')), + models.TextField(blank=True, max_length=32, null=True, verbose_name='Change secret method')), + ('push_account_enabled', models.BooleanField(default=False, verbose_name='Push account enabled')), + ('push_account_method', + models.TextField(blank=True, max_length=32, null=True, verbose_name='Push account method')), ('verify_account_enabled', models.BooleanField(default=False, verbose_name='Verify account enabled')), ('verify_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')), diff --git a/apps/assets/migrations/0097_auto_20220426_1558.py b/apps/assets/migrations/0097_auto_20220426_1558.py index f563d50d7..ec7f64084 100644 --- a/apps/assets/migrations/0097_auto_20220426_1558.py +++ b/apps/assets/migrations/0097_auto_20220426_1558.py @@ -13,6 +13,27 @@ def update_user_platforms(apps, *args): AllTypes.update_user_create_platforms(platform_cls) +def migrate_macos_platform(apps, schema_editor): + db_alias = schema_editor.connection.alias + asset_model = apps.get_model('assets', 'Asset') + platform_model = apps.get_model('assets', 'Platform') + old_macos = platform_model.objects.using(db_alias).filter( + name='MacOS', type='macos' + ).first() + new_macos = platform_model.objects.using(db_alias).filter( + name='macOS', type='unix' + ).first() + + if not old_macos or not new_macos: + return + + asset_model.objects.using(db_alias).filter( + platform=old_macos + ).update(platform=new_macos) + + platform_model.objects.using(db_alias).filter(id=old_macos.id).delete() + + class Migration(migrations.Migration): dependencies = [ ('assets', '0096_auto_20220426_1550'), @@ -21,4 +42,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(create_internal_platforms), migrations.RunPython(update_user_platforms), + migrations.RunPython(migrate_macos_platform), ] diff --git a/apps/assets/migrations/0099_auto_20220711_1409.py b/apps/assets/migrations/0099_auto_20220711_1409.py index 58f917ac4..c0444048f 100644 --- a/apps/assets/migrations/0099_auto_20220711_1409.py +++ b/apps/assets/migrations/0099_auto_20220711_1409.py @@ -1,13 +1,47 @@ # Generated by Django 3.2.12 on 2022-07-11 08:59 -import uuid +import time -import django.db.models.deletion -import simple_history.models from django.conf import settings from django.db import migrations, models -import common.db.fields + +def migrate_asset_protocols(apps, schema_editor): + asset_model = apps.get_model('assets', 'Asset') + protocol_model = apps.get_model('assets', 'Protocol') + + count = 0 + bulk_size = 1000 + print("\n\tStart migrate asset protocols") + while True: + start = time.time() + assets = asset_model.objects.all()[count:count + bulk_size] + if not assets: + break + count += len(assets) + assets_protocols = [] + + for asset in assets: + old_protocols = asset._protocols or '{}/{}'.format(asset.protocol, asset.port) or 'ssh/22' + + if ',' in old_protocols: + _protocols = old_protocols.split(',') + else: + _protocols = old_protocols.split() + + for name_port in _protocols: + name_port_list = name_port.split('/') + if len(name_port_list) != 2: + continue + + name, port = name_port_list + protocol = protocol_model(**{'name': name, 'port': port, 'asset': asset}) + assets_protocols.append(protocol) + + protocol_model.objects.bulk_create(assets_protocols, ignore_conflicts=True) + print("\t - Create asset protocols: {}-{} using: {:.2f}s".format( + count - len(assets), count, time.time() - start + )) class Migration(migrations.Migration): @@ -17,92 +51,21 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='HistoricalAccount', - fields=[ - ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('version', models.IntegerField(default=0, verbose_name='Version'),), - ('history_id', models.AutoField(primary_key=True, serialize=False)), - ('history_date', models.DateTimeField(db_index=True)), - ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', - models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('history_user', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', - to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'historical Account', - 'verbose_name_plural': 'historical Accounts', - 'ordering': ('-history_date', '-history_id'), - 'get_latest_by': ('history_date', 'history_id'), - }, - bases=(simple_history.models.HistoricalChanges, models.Model), + migrations.RenameField( + model_name='asset', + old_name='protocols', + new_name='_protocols', ), migrations.CreateModel( - name='Account', + name='Protocol', fields=[ - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], - default='unknown', max_length=16, verbose_name='Connectivity')), - ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), - ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), - ('version', models.IntegerField(default=0, verbose_name='Version')), - ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', - to='assets.asset', verbose_name='Asset')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='Name')), + ('port', models.IntegerField(verbose_name='Port')), + ('asset', + models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.asset', + verbose_name='Asset')), ], - options={ - 'verbose_name': 'Account', - 'permissions': [('view_accountsecret', 'Can view asset account secret'), - ('change_accountsecret', 'Can change asset account secret'), - ('view_historyaccount', 'Can view asset history account'), - ('view_historyaccountsecret', 'Can view asset history account secret')], - 'unique_together': {('name', 'asset'), ('username', 'asset', 'secret_type')}, - }, - ), - migrations.AddField( - model_name='account', - name='su_from', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', - to='assets.account', verbose_name='Su from'), - ), - migrations.CreateModel( - name='AccountTemplate', - fields=[ - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), - ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), - ], - options={ - 'verbose_name': 'Account template', - 'unique_together': {('name', 'org_id')}, - }, ), + migrations.RunPython(migrate_asset_protocols), ] diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py index f45ff27da..62fa1cbd5 100644 --- a/apps/assets/migrations/0100_auto_20220711_1413.py +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -7,7 +7,7 @@ from assets.models import Platform def migrate_accounts(apps, schema_editor): auth_book_model = apps.get_model('assets', 'AuthBook') - account_model = apps.get_model('assets', 'Account') + account_model = apps.get_model('accounts', 'Account') count = 0 bulk_size = 1000 @@ -15,8 +15,8 @@ def migrate_accounts(apps, schema_editor): while True: start = time.time() auth_books = auth_book_model.objects \ - .prefetch_related('systemuser') \ - .all()[count:count+bulk_size] + .prefetch_related('systemuser') \ + .all()[count:count + bulk_size] if not auth_books: break @@ -72,13 +72,13 @@ def migrate_accounts(apps, schema_editor): account_model.objects.bulk_create(accounts, ignore_conflicts=True) print("\t - Create accounts: {}-{} using: {:.2f}s".format( - count - len(auth_books), count, time.time()-start + count - len(auth_books), count, time.time() - start )) class Migration(migrations.Migration): - dependencies = [ + ('accounts', '0001_initial'), ('assets', '0099_auto_20220711_1409'), ] diff --git a/apps/assets/migrations/0101_auto_20220803_1448.py b/apps/assets/migrations/0101_auto_20220803_1448.py deleted file mode 100644 index 9a07c9593..000000000 --- a/apps/assets/migrations/0101_auto_20220803_1448.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.2.14 on 2022-08-03 10:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0100_auto_20220711_1413'), - ] - - operations = [ - migrations.RenameField( - model_name='asset', - old_name='protocols', - new_name='_protocols', - ), - migrations.CreateModel( - name='Protocol', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=32, verbose_name='Name')), - ('port', models.IntegerField(verbose_name='Port')), - ('asset', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.asset', verbose_name='Asset')), - ], - ), - ] diff --git a/apps/assets/migrations/0103_auto_20220811_1511.py b/apps/assets/migrations/0101_auto_20220811_1511.py similarity index 87% rename from apps/assets/migrations/0103_auto_20220811_1511.py rename to apps/assets/migrations/0101_auto_20220811_1511.py index b10455d2f..9ca39774e 100644 --- a/apps/assets/migrations/0103_auto_20220811_1511.py +++ b/apps/assets/migrations/0101_auto_20220811_1511.py @@ -5,16 +5,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assets', '0102_auto_20220803_1859'), + ('assets', '0100_auto_20220711_1413'), ] operations = [ migrations.AlterField( model_name='asset', name='platform', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assets', to='assets.platform', verbose_name='Platform'), + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assets', + to='assets.platform', verbose_name='Platform'), ), migrations.RemoveField( model_name='asset', diff --git a/apps/assets/migrations/0102_auto_20220803_1859.py b/apps/assets/migrations/0102_auto_20220803_1859.py deleted file mode 100644 index afaa63d41..000000000 --- a/apps/assets/migrations/0102_auto_20220803_1859.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 3.2.14 on 2022-08-03 10:59 -import time -from django.db import migrations - - -def migrate_asset_protocols(apps, schema_editor): - asset_model = apps.get_model('assets', 'Asset') - protocol_model = apps.get_model('assets', 'Protocol') - - count = 0 - bulk_size = 1000 - print("\n\tStart migrate asset protocols") - while True: - start = time.time() - assets = asset_model.objects.all()[count:count+bulk_size] - if not assets: - break - count += len(assets) - assets_protocols = [] - - for asset in assets: - old_protocols = asset._protocols or '{}/{}'.format(asset.protocol, asset.port) or 'ssh/22' - - if ',' in old_protocols: - _protocols = old_protocols.split(',') - else: - _protocols = old_protocols.split() - - for name_port in _protocols: - name_port_list = name_port.split('/') - if len(name_port_list) != 2: - continue - - name, port = name_port_list - protocol = protocol_model(**{'name': name, 'port': port, 'asset': asset}) - assets_protocols.append(protocol) - - protocol_model.objects.bulk_create(assets_protocols, ignore_conflicts=True) - print("\t - Create asset protocols: {}-{} using: {:.2f}s".format( - count - len(assets), count, time.time()-start - )) - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0101_auto_20220803_1448'), - ] - - operations = [ - migrations.RunPython(migrate_asset_protocols) - ] diff --git a/apps/assets/migrations/0104_auto_20220816_1022.py b/apps/assets/migrations/0102_auto_20220816_1022.py similarity index 94% rename from apps/assets/migrations/0104_auto_20220816_1022.py rename to apps/assets/migrations/0102_auto_20220816_1022.py index 111e4a479..922fb23e9 100644 --- a/apps/assets/migrations/0104_auto_20220816_1022.py +++ b/apps/assets/migrations/0102_auto_20220816_1022.py @@ -13,7 +13,7 @@ def migrate_command_filter_to_assets(apps, schema_editor): while True: start = time.time() command_filters = command_filter_model.objects.all() \ - .prefetch_related('system_users')[count:count + bulk_size] + .prefetch_related('system_users')[count:count + bulk_size] if not command_filters: break count += len(command_filters) @@ -44,9 +44,8 @@ def migrate_command_filter_apps(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('assets', '0103_auto_20220811_1511'), + ('assets', '0101_auto_20220811_1511'), ] operations = [ diff --git a/apps/assets/migrations/0112_gateway_to_asset.py b/apps/assets/migrations/0103_auto_20220902_1021.py similarity index 95% rename from apps/assets/migrations/0112_gateway_to_asset.py rename to apps/assets/migrations/0103_auto_20220902_1021.py index 67c874761..8d324fa12 100644 --- a/apps/assets/migrations/0112_gateway_to_asset.py +++ b/apps/assets/migrations/0103_auto_20220902_1021.py @@ -47,7 +47,7 @@ def migrate_gateway_to_asset(apps, schema_editor): print('>>> migrate gateway to account') accounts = [] - account_model = apps.get_model('assets', 'Account') + account_model = apps.get_model('accounts', 'Account') for gateway in gateways: password = gateway.password private_key = gateway.private_key @@ -66,7 +66,7 @@ def migrate_gateway_to_asset(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('assets', '0111_alter_automationexecution_status'), + ('assets', '0102_auto_20220816_1022'), ] operations = [ @@ -82,6 +82,7 @@ class Migration(migrations.Migration): 'proxy': True, 'indexes': [], 'constraints': [], + 'verbose_name': 'Gateway' }, bases=('assets.host',), ), diff --git a/apps/assets/migrations/0105_auto_20220817_1544.py b/apps/assets/migrations/0104_auto_20220817_1544.py similarity index 97% rename from apps/assets/migrations/0105_auto_20220817_1544.py rename to apps/assets/migrations/0104_auto_20220817_1544.py index 40084ffdd..864eba8f1 100644 --- a/apps/assets/migrations/0105_auto_20220817_1544.py +++ b/apps/assets/migrations/0104_auto_20220817_1544.py @@ -4,9 +4,8 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('assets', '0104_auto_20220816_1022'), + ('assets', '0103_auto_20220902_1021'), ] operations = [ diff --git a/apps/assets/migrations/0115_auto_20221220_1956.py b/apps/assets/migrations/0105_auto_20221220_1956.py similarity index 55% rename from apps/assets/migrations/0115_auto_20221220_1956.py rename to apps/assets/migrations/0105_auto_20221220_1956.py index 976a8c53b..196a33fb8 100644 --- a/apps/assets/migrations/0115_auto_20221220_1956.py +++ b/apps/assets/migrations/0105_auto_20221220_1956.py @@ -4,27 +4,11 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assets', '0114_remove_redundant_macos'), + ('assets', '0104_auto_20220817_1544'), ] operations = [ - migrations.AddField( - model_name='accountbackupplan', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='baseautomation', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='changesecretrecord', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), migrations.AddField( model_name='domain', name='created_by', @@ -50,21 +34,6 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AddField( - model_name='gathereduser', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), - migrations.AddField( - model_name='gathereduser', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AddField( - model_name='gathereduser', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), migrations.AddField( model_name='node', name='comment', @@ -90,35 +59,30 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='account', + migrations.AddField( + model_name='label', name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), ), - migrations.AlterField( - model_name='account', + migrations.AddField( + model_name='label', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='label', name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), ), migrations.AlterField( - model_name='accountbackupplan', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), + model_name='platformprotocol', + name='default', + field=models.BooleanField(default=False, verbose_name='Default'), ), migrations.AlterField( - model_name='accountbackupplan', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='accounttemplate', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='accounttemplate', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + model_name='gateway', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), ), migrations.AlterField( model_name='asset', @@ -130,26 +94,6 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='baseautomation', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), - migrations.AlterField( - model_name='baseautomation', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='changesecretrecord', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='changesecretrecord', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), migrations.AlterField( model_name='domain', name='comment', @@ -160,11 +104,6 @@ class Migration(migrations.Migration): name='created_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), ), - migrations.AlterField( - model_name='gathereduser', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), migrations.AlterField( model_name='label', name='comment', @@ -180,4 +119,9 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), + migrations.AlterField( + model_name='gateway', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), ] diff --git a/apps/assets/migrations/0106_auto_20220916_1556.py b/apps/assets/migrations/0106_auto_20220916_1556.py deleted file mode 100644 index 70db1a2f8..000000000 --- a/apps/assets/migrations/0106_auto_20220916_1556.py +++ /dev/null @@ -1,71 +0,0 @@ -# Generated by Django 3.2.13 on 2022-09-16 07:56 -from functools import reduce -from django.db import migrations, models -from assets.const import AllTypes, HostTypes - - -def migrate_backup_types(apps, schema_editor): - all_types = list(reduce( - lambda x, y: x + y, - [ - [j['value'] for j in i['children']] - for i in AllTypes.grouped_choices_to_objs() - ] - )) - asset_types = [i[0] for i in HostTypes.choices] - app_types = list(set(all_types) - set(asset_types)) - - backup_model = apps.get_model("assets", "AccountBackupPlan") - backup_objs = [] - for instance in backup_model.objects.all(): - types = instance.types - if types == 1: - instance.categories = asset_types - elif types == 2: - instance.categories = app_types - elif types == 255: - instance.categories = all_types - else: - instance.categories = [] - backup_objs.append(instance) - backup_model.objects.bulk_update(backup_objs, ['categories']) - - backup_execution_model = apps.get_model("assets", "AccountBackupPlanExecution") - backup_execution_objs = [] - for instance in backup_execution_model.objects.all(): - types = instance.plan_snapshot.get('types', []) - if 'all' in types: - instance.plan_snapshot['categories'] = all_types - elif 'asset' in types: - instance.plan_snapshot['categories'] = asset_types - elif 'application' in types: - instance.plan_snapshot['categories'] = app_types - else: - instance.categories = [] - instance.plan_snapshot.pop('types', None) - backup_execution_objs.append(instance) - backup_execution_model.objects.bulk_update(backup_execution_objs, ['plan_snapshot']) - - -class Migration(migrations.Migration): - dependencies = [ - ('assets', '0105_auto_20220817_1544'), - ] - - operations = [ - migrations.AlterField( - model_name='accountbackupplan', - name='types', - field=models.BigIntegerField(), - ), - migrations.AddField( - model_name='accountbackupplan', - name='categories', - field=models.JSONField(default=list), - ), - migrations.RunPython(migrate_backup_types), - migrations.RemoveField( - model_name='accountbackupplan', - name='types', - ), - ] diff --git a/apps/assets/migrations/0106_auto_20221228_1838.py b/apps/assets/migrations/0106_auto_20221228_1838.py new file mode 100644 index 000000000..9637bbd1d --- /dev/null +++ b/apps/assets/migrations/0106_auto_20221228_1838.py @@ -0,0 +1,84 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:38 + +import common.db.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0105_auto_20221220_1956'), + ('tickets', '0025_auto_20221206_1820'), + ] + + operations = [ + migrations.RemoveField( + model_name='accountbackupplanexecution', + name='plan', + ), + migrations.AlterUniqueTogether( + name='commandfilter', + unique_together=None, + ), + migrations.RemoveField( + model_name='commandfilter', + name='assets', + ), + migrations.RemoveField( + model_name='commandfilter', + name='nodes', + ), + migrations.RemoveField( + model_name='commandfilter', + name='user_groups', + ), + migrations.RemoveField( + model_name='commandfilter', + name='users', + ), + migrations.RemoveField( + model_name='commandfilterrule', + name='filter', + ), + migrations.RemoveField( + model_name='commandfilterrule', + name='reviewers', + ), + migrations.RemoveField( + model_name='gathereduser', + name='asset', + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['name'], + 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), + ('test_assetconnectivity', 'Can test asset connectivity'), + ('push_assetaccount', 'Can push account to asset'), + ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), + ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, + ), + migrations.AlterUniqueTogether( + name='accountbackupplan', + unique_together=None, + ), + migrations.RemoveField( + model_name='accountbackupplan', + name='recipients', + ), + migrations.DeleteModel( + name='AccountBackupPlanExecution', + ), + migrations.DeleteModel( + name='CommandFilter', + ), + migrations.DeleteModel( + name='CommandFilterRule', + ), + migrations.DeleteModel( + name='GatheredUser', + ), + migrations.DeleteModel( + name='AccountBackupPlan', + ), + ] diff --git a/apps/assets/migrations/0107_auto_20221019_1115.py b/apps/assets/migrations/0107_auto_20221019_1115.py deleted file mode 100644 index 7951f56f1..000000000 --- a/apps/assets/migrations/0107_auto_20221019_1115.py +++ /dev/null @@ -1,175 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-19 03:15 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import common.db.fields - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('assets', '0106_auto_20220916_1556'), - ] - - operations = [ - migrations.CreateModel( - name='AutomationExecution', - fields=[ - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='pending', max_length=16)), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, - verbose_name='Automation snapshot')), - ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], - default='manual', max_length=128, verbose_name='Trigger mode')), - ], - options={ - 'verbose_name': 'Automation task execution', - }, - ), - migrations.CreateModel( - name='BaseAutomation', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('accounts', models.JSONField(default=list, verbose_name='Accounts')), - ('type', models.CharField(max_length=16, verbose_name='Type')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')), - ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')), - ], - options={ - 'verbose_name': 'Automation task', - 'unique_together': {('org_id', 'name')}, - }, - ), - migrations.AddField( - model_name='label', - name='created_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), - ), - migrations.AddField( - model_name='label', - name='date_updated', - field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), - ), - migrations.AddField( - model_name='label', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='platformprotocol', - name='default', - field=models.BooleanField(default=False, verbose_name='Default'), - ), - migrations.CreateModel( - name='GatherFactsAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Gather asset facts', - }, - bases=('assets.baseautomation',), - ), - migrations.CreateModel( - name='PushAccountAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Push asset account', - }, - bases=('assets.baseautomation',), - ), - migrations.CreateModel( - name='VerifyAccountAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Verify asset account', - }, - bases=('assets.baseautomation',), - ), - migrations.CreateModel( - name='ChangeSecretRecord', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')), - ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), - ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), - ('status', models.CharField(default='pending', max_length=16)), - ('error', models.TextField(blank=True, null=True, verbose_name='Error')), - ('account', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.account')), - ('execution', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.automationexecution')), - ], - options={ - 'verbose_name': 'Change secret record', - }, - ), - migrations.AddField( - model_name='automationexecution', - name='automation', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', - to='assets.baseautomation', verbose_name='Automation task'), - ), - migrations.CreateModel( - name='ChangeSecretAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), - ('secret_strategy', models.CharField( - choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), - ('random_all', 'All assets use different random password')], default='specific', - max_length=16, verbose_name='Secret strategy')), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), - ('ssh_key_change_strategy', models.CharField( - choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), - ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, - verbose_name='SSH key change strategy')), - ('recipients', - models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), - ], - options={ - 'verbose_name': 'Change secret automation', - }, - bases=('assets.baseautomation',), - ), - ] diff --git a/apps/assets/migrations/0107_automation.py b/apps/assets/migrations/0107_automation.py new file mode 100644 index 000000000..5711481e7 --- /dev/null +++ b/apps/assets/migrations/0107_automation.py @@ -0,0 +1,91 @@ +# Generated by Django 3.2.16 on 2022-12-30 08:08 + +import common.db.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0106_auto_20221228_1838'), + ] + + operations = [ + migrations.CreateModel( + name='BaseAutomation', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('accounts', models.JSONField(default=list, verbose_name='Accounts')), + ('type', models.CharField(max_length=16, verbose_name='Type')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')), + ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')), + ], + options={ + 'verbose_name': 'Automation task', + 'unique_together': {('org_id', 'name')}, + }, + ), + migrations.CreateModel( + name='AutomationExecution', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, verbose_name='Automation snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')), + ('automation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation task')), + ], + options={ + 'verbose_name': 'Automation task execution', + 'ordering': ('-date_start',), + }, + ), + migrations.CreateModel( + name='AssetBaseAutomation', + fields=[ + ], + options={ + 'verbose_name': 'Asset automation task', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='GatherFactsAutomation', + fields=[ + ('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Gather asset facts', + }, + bases=('assets.assetbaseautomation',), + ), + migrations.CreateModel( + name='PingAutomation', + fields=[ + ('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Ping asset', + }, + bases=('assets.assetbaseautomation',), + ), + ] diff --git a/apps/assets/migrations/0108_alter_automationexecution_automation.py b/apps/assets/migrations/0108_alter_automationexecution_automation.py new file mode 100644 index 000000000..7fa692b2b --- /dev/null +++ b/apps/assets/migrations/0108_alter_automationexecution_automation.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.16 on 2023-01-05 06:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0107_automation'), + ] + + operations = [ + migrations.AlterField( + model_name='automationexecution', + name='automation', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation task'), + ), + ] diff --git a/apps/assets/migrations/0108_auto_20221027_1053.py b/apps/assets/migrations/0108_auto_20221027_1053.py deleted file mode 100644 index 0aa1b5c7a..000000000 --- a/apps/assets/migrations/0108_auto_20221027_1053.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-27 02:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0107_auto_20221019_1115'), - ] - - operations = [ - migrations.CreateModel( - name='GatherAccountsAutomation', - fields=[ - ('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Gather asset accounts', - }, - bases=('assets.baseautomation',), - ), - migrations.AlterField( - model_name='baseautomation', - name='type', - field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Create account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'), - ), - migrations.CreateModel( - name='PingAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Ping asset', - }, - bases=('assets.baseautomation',), - ), - migrations.AlterModelOptions( - name='asset', - options={'ordering': ['name'], - 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), - ('test_assetconnectivity', 'Can test asset connectivity'), - ('push_assetaccount', 'Can push account to asset'), - ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), - ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, - ), - ] diff --git a/apps/assets/migrations/0109_alter_baseautomation_unique_together.py b/apps/assets/migrations/0109_alter_baseautomation_unique_together.py new file mode 100644 index 000000000..a1d141585 --- /dev/null +++ b/apps/assets/migrations/0109_alter_baseautomation_unique_together.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2023-01-06 07:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0108_alter_automationexecution_automation'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='baseautomation', + unique_together={('org_id', 'name', 'type')}, + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['name'], + 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), + ('test_assetconnectivity', 'Can test asset connectivity'), + ('push_assetaccount', 'Can push account to asset'), + ('test_account', 'Can verify account'), ('match_asset', 'Can match asset'), + ('add_assettonode', 'Add asset to node'), + ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, + ), + ] diff --git a/apps/assets/migrations/0109_auto_20221102_2017.py b/apps/assets/migrations/0109_auto_20221102_2017.py deleted file mode 100644 index a9dcc4446..000000000 --- a/apps/assets/migrations/0109_auto_20221102_2017.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-02 12:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0108_auto_20221027_1053'), - ] - - operations = [ - migrations.AddField( - model_name='account', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active'), - ), - migrations.AddField( - model_name='account', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='accounttemplate', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active'), - ), - migrations.AddField( - model_name='accounttemplate', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='gateway', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='account', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), - migrations.AlterField( - model_name='accounttemplate', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), - migrations.AlterField( - model_name='gateway', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), - ] diff --git a/apps/assets/migrations/0110_alter_favoriteasset_options.py b/apps/assets/migrations/0110_alter_favoriteasset_options.py new file mode 100644 index 000000000..0aec8be61 --- /dev/null +++ b/apps/assets/migrations/0110_alter_favoriteasset_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0109_alter_baseautomation_unique_together'), + ] + + operations = [ + migrations.AlterModelOptions( + name='favoriteasset', + options={'verbose_name': 'Favorite Asset'}, + ), + ] diff --git a/apps/assets/migrations/0110_changesecretrecord_asset.py b/apps/assets/migrations/0110_changesecretrecord_asset.py deleted file mode 100644 index 76a3f9872..000000000 --- a/apps/assets/migrations/0110_changesecretrecord_asset.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-03 13:57 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0109_auto_20221102_2017'), - ] - - operations = [ - migrations.RenameField( - model_name='accountbackupplan', - old_name='categories', - new_name='types', - ), - migrations.AddField( - model_name='changesecretrecord', - name='asset', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset'), - ), - ] diff --git a/apps/assets/migrations/0111_alter_automationexecution_status.py b/apps/assets/migrations/0111_alter_automationexecution_status.py deleted file mode 100644 index 5ccaf638f..000000000 --- a/apps/assets/migrations/0111_alter_automationexecution_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-11 11:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0110_changesecretrecord_asset'), - ] - - operations = [ - migrations.AlterField( - model_name='automationexecution', - name='status', - field=models.CharField(default='pending', max_length=16, verbose_name='Status'), - ), - ] diff --git a/apps/assets/migrations/0113_auto_20221122_2015.py b/apps/assets/migrations/0113_auto_20221122_2015.py deleted file mode 100644 index 9488b5499..000000000 --- a/apps/assets/migrations/0113_auto_20221122_2015.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-28 10:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('assets', '0112_gateway_to_asset'), - ] - - operations = [ - migrations.AlterModelOptions( - name='accounttemplate', - options={'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), - ('change_accounttemplatesecret', 'Can change asset account template secret')], - 'verbose_name': 'Account template'}, - ), - migrations.AddField( - model_name='database', - name='allow_invalid_cert', - field=models.BooleanField(default=False, verbose_name='Allow invalid cert'), - ), - migrations.AddField( - model_name='database', - name='ca_cert', - field=models.TextField(blank=True, verbose_name='CA cert'), - ), - migrations.AddField( - model_name='database', - name='client_cert', - field=models.TextField(blank=True, verbose_name='Client cert'), - ), - migrations.AddField( - model_name='database', - name='client_key', - field=models.TextField(blank=True, verbose_name='Client key'), - ), - migrations.AddField( - model_name='database', - name='use_ssl', - field=models.BooleanField(default=False, verbose_name='Use SSL'), - ), - ] diff --git a/apps/assets/migrations/0114_remove_redundant_macos.py b/apps/assets/migrations/0114_remove_redundant_macos.py deleted file mode 100644 index d24a3e74a..000000000 --- a/apps/assets/migrations/0114_remove_redundant_macos.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-15 07:08 - -from django.db import migrations - - -def migrate_del_macos(apps, schema_editor): - db_alias = schema_editor.connection.alias - asset_model = apps.get_model('assets', 'Asset') - platform_model = apps.get_model('assets', 'Platform') - old_macos = platform_model.objects.using(db_alias).filter( - name='MacOS', type='macos' - ).first() - new_macos = platform_model.objects.using(db_alias).filter( - name='macOS', type='unix' - ).first() - - if not old_macos or not new_macos: - return - - asset_model.objects.using(db_alias).filter( - platform=old_macos - ).update(platform=new_macos) - - platform_model.objects.using(db_alias).filter(id=old_macos.id).delete() - - -class Migration(migrations.Migration): - dependencies = [ - ('assets', '0113_auto_20221122_2015'), - ] - - operations = [ - migrations.RunPython(migrate_del_macos), - ] diff --git a/apps/assets/migrations/0116_alter_automationexecution_options.py b/apps/assets/migrations/0116_alter_automationexecution_options.py deleted file mode 100644 index 75ae53efd..000000000 --- a/apps/assets/migrations/0116_alter_automationexecution_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-22 11:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0115_auto_20221220_1956'), - ] - - operations = [ - migrations.AlterModelOptions( - name='automationexecution', - options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), ('add_changesecretexection', 'Can add change secret execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution')], 'verbose_name': 'Automation task execution'}, - ), - ] diff --git a/apps/assets/migrations/0117_alter_gateway_options.py b/apps/assets/migrations/0117_alter_gateway_options.py deleted file mode 100644 index 826535b3f..000000000 --- a/apps/assets/migrations/0117_alter_gateway_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-23 07:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0116_alter_automationexecution_options'), - ] - - operations = [ - migrations.AlterModelOptions( - name='gateway', - options={'verbose_name': 'Gateway'}, - ), - ] diff --git a/apps/assets/migrations/0118_auto_20221227_1504.py b/apps/assets/migrations/0118_auto_20221227_1504.py deleted file mode 100644 index 47452733a..000000000 --- a/apps/assets/migrations/0118_auto_20221227_1504.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-27 07:04 - -import common.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0117_alter_gateway_options'), - ] - - operations = [ - migrations.AddField( - model_name='pushaccountautomation', - name='password_rules', - field=models.JSONField(default=dict, verbose_name='Password rules'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='secret', - field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='secret_strategy', - field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='specific', max_length=16, verbose_name='Secret strategy'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='secret_type', - field=models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='ssh_key_change_strategy', - field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='username', - field=models.CharField(default='', max_length=128, verbose_name='Username'), - preserve_default=False, - ), - migrations.AlterField( - model_name='baseautomation', - name='type', - field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Push account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'), - ), - ] diff --git a/apps/assets/migrations/0119_auto_20221227_1740.py b/apps/assets/migrations/0119_auto_20221227_1740.py deleted file mode 100644 index 63048163d..000000000 --- a/apps/assets/migrations/0119_auto_20221227_1740.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-27 09:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0118_auto_20221227_1504'), - ] - - operations = [ - migrations.AddField( - model_name='account', - name='source', - field=models.CharField(default='local', max_length=30, verbose_name='Source'), - ), - migrations.DeleteModel( - name='GatheredUser', - ), - ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 78e6e579d..5eeaf2626 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -6,13 +6,9 @@ from .group import * from .gateway import * from .domain import * from .node import * -from .utils import * from .favorite_asset import * -from .account import * -from .backup import * from .automations import * -from ._user import * # 废弃以下 # from ._authbook import * -from .cmd_filter import * - +# from .cmd_filter import * +from ._user import * diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 6ef10b019..afe905ac3 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -2,12 +2,14 @@ # -*- coding: utf-8 -*- # +import json import logging from collections import defaultdict from django.db import models from django.utils.translation import ugettext_lazy as _ +from assets import const from common.utils import lazyproperty from orgs.mixins.models import OrgManager, JMSOrgBaseModel from ..base import AbsConnectivity @@ -98,6 +100,9 @@ class Protocol(models.Model): class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): + Category = const.Category + Type = const.AllTypes + name = models.CharField(max_length=128, verbose_name=_('Name')) address = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') @@ -116,11 +121,53 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): @property def specific(self): - if not hasattr(self, self.category): + instance = getattr(self, self.category, None) + if not instance: return {} - instance = getattr(self, self.category) - private_fields = [i.name for i in instance._meta.local_fields if i.name != 'asset_ptr'] - return {i: getattr(instance, i) for i in private_fields} + specific_fields = self.get_specific_fields(instance) + info = {} + for i in specific_fields: + v = getattr(instance, i.name) + if isinstance(i, models.JSONField) and not isinstance(v, (list, dict)): + v = json.loads(v) + info[i.name] = v + return info + + @property + def spec_info(self): + instance = getattr(self, self.category, None) + if not instance: + return [] + specific_fields = self.get_specific_fields(instance) + info = [ + { + 'label': i.verbose_name, + 'name': i.name, + 'value': getattr(instance, i.name) + } + for i in specific_fields + ] + return info + + @lazyproperty + def enabled_info(self): + platform = self.platform + automation = self.platform.automation + return { + 'su_enabled': platform.su_enabled, + 'ping_enabled': automation.ping_enabled, + 'domain_enabled': platform.domain_enabled, + 'ansible_enabled': automation.ansible_enabled, + 'gather_facts_enabled': automation.gather_facts_enabled, + 'change_secret_enabled': automation.change_secret_enabled, + 'verify_account_enabled': automation.verify_account_enabled, + 'gather_accounts_enabled': automation.gather_accounts_enabled, + } + + @staticmethod + def get_specific_fields(instance): + specific_fields = [i for i in instance._meta.local_fields if i.name != 'asset_ptr'] + return specific_fields def get_target_ip(self): return self.address @@ -177,6 +224,18 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): def category(self): return self.platform.category + def is_category(self, category): + return self.category == category + + def is_type(self, tp): + return self.type == tp + + @lazyproperty + def gateway(self): + if self.domain_id: + return self.domain.select_gateway() + return None + def as_node(self): from assets.models import Node fake_node = Node() @@ -224,6 +283,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): ('refresh_assethardwareinfo', _('Can refresh asset hardware info')), ('test_assetconnectivity', _('Can test asset connectivity')), ('push_assetaccount', _('Can push account to asset')), + ('test_account', _('Can verify account')), ('match_asset', _('Can match asset')), ('add_assettonode', _('Add asset to node')), ('move_assettonode', _('Move asset to node')), diff --git a/apps/assets/models/asset/database.py b/apps/assets/models/asset/database.py index 4772a6b08..2c033de9e 100644 --- a/apps/assets/models/asset/database.py +++ b/apps/assets/models/asset/database.py @@ -15,20 +15,17 @@ class Database(Asset): def __str__(self): return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name) - @property - def ip(self): - return self.address - @property def specific(self): return { 'db_name': self.db_name, 'use_ssl': self.use_ssl, - 'ca_cert': self.ca_cert, - 'client_cert': self.client_cert, - 'client_key': self.client_key, 'allow_invalid_cert': self.allow_invalid_cert, } + @property + def ip(self): + return self.address + class Meta: verbose_name = _("Database") diff --git a/apps/assets/models/automations/__init__.py b/apps/assets/models/automations/__init__.py index abf23ed7e..f6665cfbb 100644 --- a/apps/assets/models/automations/__init__.py +++ b/apps/assets/models/automations/__init__.py @@ -1,7 +1,3 @@ -from .ping import * from .base import * -from .push_account import * from .gather_facts import * -from .change_secret import * -from .verify_account import * -from .gather_accounts import * +from .ping import * diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 6504e5898..e888fdf26 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -4,12 +4,12 @@ from celery import current_task from django.db import models from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes -from assets.models import Node, Asset +from assets.models.node import Node +from assets.models.asset import Asset from assets.tasks import execute_automation +from ops.mixin import PeriodTaskModelMixin from common.const.choices import Trigger from common.db.fields import EncryptJsonDictTextField -from ops.mixin import PeriodTaskModelMixin from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel @@ -17,12 +17,16 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): 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")) - type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type')) + type = models.CharField(max_length=16, verbose_name=_('Type')) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) def __str__(self): return self.name + '@' + str(self.created_by) + class Meta: + unique_together = [('org_id', 'name', 'type')] + verbose_name = _("Automation task") + @classmethod def generate_unique_name(cls, name): while True: @@ -57,35 +61,45 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): return { 'name': self.name, 'type': self.type, - 'org_id': str(self.org_id), 'comment': self.comment, 'accounts': self.accounts, + 'org_id': str(self.org_id), 'nodes': self.get_many_to_many_ids('nodes'), 'assets': self.get_many_to_many_ids('assets'), } + @property + def execution_model(self): + return AutomationExecution + + @property + def executed_amount(self): + return self.executions.count() + def execute(self, trigger=Trigger.manual): try: eid = current_task.request.id except AttributeError: eid = str(uuid.uuid4()) - execution = self.executions.model.objects.create( + execution = self.execution_model.objects.create( id=eid, trigger=trigger, automation=self, snapshot=self.to_attr_json(), ) return execution.start() + +class AssetBaseAutomation(BaseAutomation): class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _("Automation task") + proxy = True + verbose_name = _("Asset automation task") class AutomationExecution(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) automation = models.ForeignKey( 'BaseAutomation', related_name='executions', on_delete=models.CASCADE, - verbose_name=_('Automation task') + verbose_name=_('Automation task'), null=True ) status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) @@ -100,24 +114,31 @@ class AutomationExecution(OrgModelMixin): ) class Meta: + ordering = ('-date_start',) verbose_name = _('Automation task execution') - permissions = [ - ('view_changesecretexecution', _('Can view change secret execution')), - ('add_changesecretexection', _('Can add change secret execution')), - ('view_gatheraccountsexecution', _('Can view gather accounts execution')), - ('add_gatheraccountsexecution', _('Can add gather accounts execution')), - ] @property def manager_type(self): return self.snapshot['type'] + def get_all_assets(self): + node_ids = self.snapshot['nodes'] + asset_ids = self.snapshot['assets'] + nodes = Node.objects.filter(id__in=node_ids) + node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) + asset_ids = set(list(asset_ids) + list(node_asset_ids)) + return Asset.objects.filter(id__in=asset_ids) + + def all_assets_group_by_platform(self): + assets = self.get_all_assets().prefetch_related('platform') + return assets.group_by_platform() + @property def recipients(self): recipients = self.snapshot.get('recipients') if not recipients: - return [] - return recipients.values() + return {} + return recipients def start(self): from assets.automations.endpoint import ExecutionManager diff --git a/apps/assets/models/automations/gather_facts.py b/apps/assets/models/automations/gather_facts.py index 1641c9f81..cf11ea41b 100644 --- a/apps/assets/models/automations/gather_facts.py +++ b/apps/assets/models/automations/gather_facts.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext_lazy as _ from assets.const import AutomationTypes -from .base import BaseAutomation +from .base import AssetBaseAutomation __all__ = ['GatherFactsAutomation'] -class GatherFactsAutomation(BaseAutomation): +class GatherFactsAutomation(AssetBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.gather_facts super().save(*args, **kwargs) diff --git a/apps/assets/models/automations/ping.py b/apps/assets/models/automations/ping.py index b327bc4ea..a8df4e7c8 100644 --- a/apps/assets/models/automations/ping.py +++ b/apps/assets/models/automations/ping.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext_lazy as _ from assets.const import AutomationTypes -from .base import BaseAutomation +from .base import AssetBaseAutomation __all__ = ['PingAutomation'] -class PingAutomation(BaseAutomation): +class PingAutomation(AssetBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.ping super().save(*args, **kwargs) diff --git a/apps/assets/models/automations/push_account.py b/apps/assets/models/automations/push_account.py deleted file mode 100644 index 33972eb58..000000000 --- a/apps/assets/models/automations/push_account.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from assets.const import AutomationTypes -from .base import BaseAutomation -from .change_secret import ChangeSecretMixin - -__all__ = ['PushAccountAutomation'] - - -class PushAccountAutomation(BaseAutomation, ChangeSecretMixin): - accounts = None - username = models.CharField(max_length=128, verbose_name=_('Username')) - - def save(self, *args, **kwargs): - self.type = AutomationTypes.push_account - super().save(*args, **kwargs) - - class Meta: - verbose_name = _("Push asset account") diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 8c8c08555..8f5ed713f 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -1,21 +1,14 @@ # -*- coding: utf-8 -*- # -import os -from hashlib import md5 -import sshpubkeys -from django.conf import settings from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from assets.const import Connectivity, SecretType -from common.db import fields +from assets.const import Connectivity from common.utils import ( - ssh_key_string_to_obj, ssh_key_gen, get_logger, - random_string, lazyproperty, parse_ssh_public_key_str + get_logger ) -from orgs.mixins.models import JMSOrgBaseModel logger = get_logger(__file__) @@ -48,130 +41,3 @@ class AbsConnectivity(models.Model): class Meta: abstract = True - - -class BaseAccountQuerySet(models.QuerySet): - def active(self): - return self.filter(is_active=True) - - -class BaseAccountManager(models.Manager): - def active(self): - return self.get_queryset().active() - - -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') - ) - secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) - privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) - is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - - objects = BaseAccountManager.from_queryset(BaseAccountQuerySet)() - - @property - def has_secret(self): - return bool(self.secret) - - @property - def has_username(self): - return bool(self.username) - - @property - def specific(self): - data = {} - 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: - return self.secret - return None - - @private_key.setter - def private_key(self, value): - self.secret = value - self.secret_type = SecretType.SSH_KEY - - @lazyproperty - def public_key(self): - if self.secret_type == SecretType.SSH_KEY and self.private_key: - return parse_ssh_public_key_str(self.private_key) - return None - - @property - def ssh_key_fingerprint(self): - if self.public_key: - public_key = self.public_key - elif self.private_key: - try: - public_key = parse_ssh_public_key_str(self.private_key) - except IOError as e: - return str(e) - else: - return '' - - public_key_obj = sshpubkeys.SSHKey(public_key) - fingerprint = public_key_obj.hash_md5() - return fingerprint - - @property - def private_key_obj(self): - if self.private_key: - key_obj = ssh_key_string_to_obj(self.private_key) - return key_obj - else: - return None - - @property - def private_key_path(self): - 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') - key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() - key_path = os.path.join(tmp_dir, key_name) - if not os.path.exists(key_path): - self.private_key_obj.write_private_key_file(key_path) - os.chmod(key_path, 0o400) - return key_path - - def get_private_key(self): - if not self.private_key: - return None - return self.private_key - - @property - def public_key_obj(self): - if self.public_key: - try: - return sshpubkeys.SSHKey(self.public_key) - except TabError: - pass - return None - - @staticmethod - def gen_password(length=36): - return random_string(length, special_char=True) - - @staticmethod - def gen_key(username): - private_key, public_key = ssh_key_gen(username=username) - return private_key, public_key - - def _to_secret_json(self): - """Push system user use it""" - return { - 'name': self.name, - 'username': self.username, - 'public_key': self.public_key, - } - - class Meta: - abstract = True diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 4e2cfc71d..dfb0e32fe 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -5,7 +5,7 @@ import random from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, lazyproperty +from common.utils import get_logger from orgs.mixins.models import JMSOrgBaseModel from .gateway import Gateway @@ -31,17 +31,21 @@ class Domain(JMSOrgBaseModel): def random_gateway(self): gateways = [gw for gw in self.active_gateways if gw.is_connective] if not gateways: - gateways = self.active_gateways logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(gateways)}.') + gateways = self.active_gateways + if not gateways: + logger.warn(f'Not active gateway. domain={self}') + return None return random.choice(gateways) - @lazyproperty + @property def active_gateways(self): return self.gateways.filter(is_active=True) - @lazyproperty + @property def gateways(self): - return self.get_gateway_queryset().filter(domain=self) + queryset = self.get_gateway_queryset().filter(domain=self) + return queryset @classmethod def get_gateway_queryset(cls): diff --git a/apps/assets/models/favorite_asset.py b/apps/assets/models/favorite_asset.py index 8fbaeed64..86d64bbe1 100644 --- a/apps/assets/models/favorite_asset.py +++ b/apps/assets/models/favorite_asset.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from django.db import models +from django.utils.translation import ugettext_lazy as _ from common.db.models import JMSBaseModel @@ -13,7 +14,11 @@ class FavoriteAsset(JMSBaseModel): class Meta: unique_together = ('user', 'asset') + verbose_name = _("Favorite Asset") @classmethod def get_user_favorite_asset_ids(cls, user): return cls.objects.filter(user=user).values_list('asset', flat=True) + + def __str__(self): + return '%s' % self.asset diff --git a/apps/assets/models/gateway.py b/apps/assets/models/gateway.py index f94ae826b..d9d4d891b 100644 --- a/apps/assets/models/gateway.py +++ b/apps/assets/models/gateway.py @@ -1,16 +1,11 @@ # -*- coding: utf-8 -*- # -import socket -import paramiko - from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, lazyproperty from orgs.mixins.models import OrgManager - -from assets.const import GATEWAY_NAME, Connectivity from assets.models.platform import Platform -from assets.models.account import Account +from assets.const import GATEWAY_NAME +from common.utils import get_logger, lazyproperty from .asset.host import Host @@ -52,71 +47,22 @@ class Gateway(Host): account = self.accounts.active().order_by('-privileged', '-date_updated').first() return account - def test_connective(self, local_port=None): - local_port = self.port if local_port is None else local_port - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - proxy = paramiko.SSHClient() - proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - if not isinstance(self.select_account, Account): - err = _('No account') - return False, err + @lazyproperty + def username(self): + account = self.select_account + return account.username if account else None - logger.debug('Test account: {}'.format(self.select_account)) - try: - proxy.connect( - self.address, - port=self.port, - username=self.select_account.username, - password=self.select_account.secret, - pkey=self.select_account.private_key_obj - ) - except( - paramiko.AuthenticationException, - paramiko.BadAuthenticationType, - paramiko.SSHException, - paramiko.ChannelException, - paramiko.ssh_exception.NoValidConnectionsError, - socket.gaierror - ) as e: - err = str(e) - if err.startswith('[Errno None] Unable to connect to port'): - err = _('Unable to connect to port {port} on {address}') - err = err.format(port=self.port, address=self.address) - elif err == 'Authentication failed.': - err = _('Authentication failed') - elif err == 'Connect failed': - err = _('Connect failed') - self.set_connectivity(Connectivity.FAILED) - return False, err + @lazyproperty + def password(self): + account = self.select_account + return account.password if account else None - try: - sock = proxy.get_transport().open_channel( - 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0) - ) - client.connect( - '127.0.0.1', - sock=sock, - timeout=5, - port=local_port, - username=self.select_account.username, - password=self.select_account.secret, - key_filename=self.select_account.private_key_path, - ) - except ( - paramiko.SSHException, - paramiko.ssh_exception.SSHException, - paramiko.ChannelException, - paramiko.AuthenticationException, - TimeoutError - ) as e: + @lazyproperty + def private_key(self): + account = self.select_account + return account.private_key if account else None - err = getattr(e, 'text', str(e)) - if err == 'Connect failed': - err = _('Connect failed') - self.set_connectivity(Connectivity.FAILED) - return False, err - finally: - client.close() - self.set_connectivity(Connectivity.OK) - return True, None + @lazyproperty + def private_key_path(self): + account = self.select_account + return account.private_key_path if account else None diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 626f931d0..5d1e633f5 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -436,6 +436,12 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin): "org_id", "is_active" ).prefetch_related('platform') + def get_all_assets_for_tree(self): + return self.get_all_assets().only( + "id", "name", "address", "platform_id", + "org_id", "is_active" + ).prefetch_related('platform') + def get_valid_assets(self): return self.get_assets().valid() diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index c013dd2d6..1bb560e7e 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -44,9 +44,14 @@ class PlatformAutomation(models.Model): ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method")) gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")) - change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled")) + change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled")) change_secret_method = models.TextField( - max_length=32, blank=True, null=True, verbose_name=_("Change password method")) + max_length=32, blank=True, null=True, verbose_name=_("Change secret method") + ) + 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") + ) 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")) @@ -77,7 +82,6 @@ class Platform(models.Model): 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")) # 账号有关的 su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled")) su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method")) diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index 891c5b9d6..a5e4da1da 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -18,64 +18,3 @@ def private_key_validator(value): _('%(value)s is not an even number'), params={'value': value}, ) - - -def update_internal_platforms(platform_model): - platforms = [ - {'name': 'Linux', 'category': 'host', 'type': 'linux'}, - {'name': 'BSD', 'category': 'host', 'type': 'unix'}, - {'name': 'Unix', 'category': 'host', 'type': 'unix'}, - {'name': 'MacOS', 'category': 'host', 'type': 'unix'}, - {'name': 'Windows', 'category': 'host', 'type': 'unix'}, - { - 'name': 'AIX', 'category': 'host', 'type': 'unix', - 'change_secret_method': 'change_secret_aix', - }, - {'name': 'Windows', 'category': 'host', 'type': 'windows'}, - { - 'name': 'Windows-TLS', 'category': 'host', 'type': 'windows', - 'protocols': [ - {'name': 'rdp', 'port': 3389, 'setting': {'security': 'tls'}}, - {'name': 'ssh', 'port': 22}, - ] - }, - { - 'name': 'Windows-RDP', 'category': 'host', 'type': 'windows', - 'protocols': [ - {'name': 'rdp', 'port': 3389, 'setting': {'security': 'rdp'}}, - {'name': 'ssh', 'port': 22}, - ] - }, - # 数据库 - {'name': 'MySQL', 'category': 'database', 'type': 'mysql'}, - {'name': 'PostgreSQL', 'category': 'database', 'type': 'postgresql'}, - {'name': 'Oracle', 'category': 'database', 'type': 'oracle'}, - {'name': 'SQLServer', 'category': 'database', 'type': 'sqlserver'}, - {'name': 'MongoDB', 'category': 'database', 'type': 'mongodb'}, - {'name': 'Redis', 'category': 'database', 'type': 'redis'}, - - # 网络设备 - {'name': 'Generic', 'category': 'device', 'type': 'general'}, - {'name': 'Huawei', 'category': 'device', 'type': 'general'}, - {'name': 'Cisco', 'category': 'device', 'type': 'general'}, - {'name': 'H3C', 'category': 'device', 'type': 'general'}, - - # Web - {'name': 'Website', 'category': 'web', 'type': 'general'}, - - # Cloud - {'name': 'Kubernetes', 'category': 'cloud', 'type': 'k8s'}, - {'name': 'VMware vSphere', 'category': 'cloud', 'type': 'private'}, - ] - - platforms = platform_model.objects.all() - - updated = [] - for p in platforms: - attrs = platform_ops_map.get((p.category, p.type), {}) - if not attrs: - continue - for k, v in attrs.items(): - setattr(p, k, v) - updated.append(p) - platform_model.objects.bulk_update(updated, list(default_ok.keys())) diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index f70a94bbf..cbf21454d 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -7,7 +7,6 @@ from .node import * from .gateway import * from .domain import * from .favorite_asset import * -from .account import * from .platform import * from .cagegory import * from .automations import * diff --git a/apps/assets/serializers/account/base.py b/apps/assets/serializers/account/base.py deleted file mode 100644 index becfd4df9..000000000 --- a/apps/assets/serializers/account/base.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers - -from assets.models import BaseAccount -from assets.serializers.base import AuthValidateMixin -from orgs.mixins.serializers import BulkOrgResourceModelSerializer - -__all__ = ['BaseAccountSerializer'] - - -class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): - has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) - - class Meta: - model = BaseAccount - fields_mini = ['id', 'name', 'username'] - 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 = [ - 'has_secret', 'specific', - 'date_verified', 'created_by', 'date_created', - ] - extra_kwargs = { - 'specific': {'label': _('Specific')}, - } diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index e2a84fe5a..30adcbe78 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -6,24 +6,25 @@ from django.db.transaction import atomic from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField, ObjectRelatedField -from common.drf.serializers import WritableNestedModelSerializer -from orgs.mixins.serializers import BulkOrgResourceSerializerMixin -from ..account import AccountSerializer +from accounts.models import Account, AccountTemplate +from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer +from common.serializers.fields import LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ...const import Category, AllTypes -from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol +from ...models import Asset, Node, Platform, Label, Protocol __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', - 'AssetDetailSerializer', + 'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer', + 'AccountSecretSerializer' ] class AssetProtocolsSerializer(serializers.ModelSerializer): class Meta: model = Protocol - fields = ['id', 'name', 'port'] + fields = ['name', 'port'] class AssetLabelSerializer(serializers.ModelSerializer): @@ -45,10 +46,14 @@ class AssetPlatformSerializer(serializers.ModelSerializer): } -class AssetAccountSerializer(AccountSerializer): +class AssetAccountSerializer(CommonModelSerializer): add_org_fields = False + push_now = serializers.BooleanField( + default=False, label=_("Push now"), write_only=True + ) - class Meta(AccountSerializer.Meta): + class Meta: + model = Account fields_mini = [ 'id', 'name', 'username', 'privileged', 'version', 'secret_type', @@ -57,28 +62,79 @@ class AssetAccountSerializer(AccountSerializer): 'secret', 'push_now' ] fields = fields_mini + fields_write_only + extra_kwargs = { + 'secret': {'write_only': True}, + } + + def validate_name(self, value): + if not value: + value = self.initial_data.get('username') + return value + + @staticmethod + def validate_template(value): + try: + return AccountTemplate.objects.get(id=value) + except AccountTemplate.DoesNotExist: + raise serializers.ValidationError(_('Account template not found')) + + @staticmethod + def replace_attrs(account_template: AccountTemplate, attrs: dict): + exclude_fields = [ + '_state', 'org_id', 'id', 'date_created', + 'date_updated' + ] + template_attrs = { + k: v for k, v in account_template.__dict__.items() + if k not in exclude_fields + } + for k, v in template_attrs.items(): + attrs.setdefault(k, v) + + def validate(self, attrs): + account_template = attrs.pop('template', None) + if account_template: + self.replace_attrs(account_template, attrs) + self.push_now = attrs.pop('push_now', False) + return super().validate(attrs) + + def create(self, validated_data): + from accounts.tasks import push_accounts_to_assets + instance = super().create(validated_data) + if self.push_now: + push_accounts_to_assets.delay([instance.id], [instance.asset_id]) + return instance -class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSerializer): +class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer): + class Meta: + model = Account + fields = [ + 'name', 'username', 'privileged', 'secret_type', 'secret', + ] + extra_kwargs = { + 'secret': {'write_only': False}, + } + + +class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer): category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) - domain = ObjectRelatedField(required=False, queryset=Domain.objects, label=_('Domain'), allow_null=True) - platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) - nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes')) - labels = AssetLabelSerializer(many=True, required=False, label=_('Labels')) + labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) - accounts = AssetAccountSerializer(many=True, required=False, label=_('Account')) + accounts = AssetAccountSerializer(many=True, required=False, write_only=True, label=_('Account')) + enabled_info = serializers.DictField(read_only=True, label=_('Enabled info')) class Meta: model = Asset fields_mini = ['id', 'name', 'address'] fields_small = fields_mini + ['is_active', 'comment'] - fields_fk = ['domain', 'platform', 'platform'] + fields_fk = ['domain', 'platform'] fields_m2m = [ - 'nodes', 'labels', 'protocols', 'accounts', 'nodes_display', + 'nodes', 'labels', 'protocols', 'nodes_display', 'accounts' ] read_only_fields = [ - 'category', 'type', 'info', + 'category', 'type', 'info', 'enabled_info', 'connectivity', 'date_verified', 'created_by', 'date_created' ] @@ -89,16 +145,31 @@ class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSeriali 'nodes_display': {'label': _('Node path')}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._init_field_choices() + + def _init_field_choices(self): + request = self.context.get('request') + if not request: + return + category = request.path.strip('/').split('/')[-1].rstrip('s') + field_category = self.fields.get('category') + field_category._choices = Category.filter_choices(category) + field_type = self.fields.get('type') + field_type._choices = AllTypes.filter_choices(category) + @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('domain', 'platform', 'protocols') \ + queryset = queryset.prefetch_related('domain', 'platform') \ .annotate(category=F("platform__category")) \ .annotate(type=F("platform__type")) - queryset = queryset.prefetch_related('nodes', 'labels', 'accounts') + queryset = queryset.prefetch_related('nodes', 'labels', 'protocols') return queryset - def perform_nodes_display_create(self, instance, nodes_display): + @staticmethod + def perform_nodes_display_create(instance, nodes_display): if not nodes_display: return nodes_to_set = [] @@ -166,28 +237,20 @@ class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSeriali return instance -class AssetDetailSerializer(AssetSerializer): +class DetailMixin(serializers.Serializer): accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) - enabled_info = serializers.SerializerMethodField() - class Meta(AssetSerializer.Meta): - fields = AssetSerializer.Meta.fields + ['accounts', 'enabled_info', 'info', 'specific'] - @staticmethod - def get_enabled_info(obj): - platform = obj.platform - automation = platform.automation - return { - 'su_enabled': platform.su_enabled, - 'ping_enabled': automation.ping_enabled, - 'domain_enabled': platform.domain_enabled, - 'ansible_enabled': automation.ansible_enabled, - 'protocols_enabled': platform.protocols_enabled, - 'gather_facts_enabled': automation.gather_facts_enabled, - 'change_secret_enabled': automation.change_secret_enabled, - 'verify_account_enabled': automation.verify_account_enabled, - 'gather_accounts_enabled': automation.gather_accounts_enabled, - } + def get_field_names(self, declared_fields, info): + names = super().get_field_names(declared_fields, info) + names.extend([ + 'accounts', 'info', 'specific', 'spec_info' + ]) + return names + + +class AssetDetailSerializer(DetailMixin, AssetSerializer): + pass class MiniAssetSerializer(serializers.ModelSerializer): diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py index d168d2ffe..c4ad0171d 100644 --- a/apps/assets/serializers/asset/database.py +++ b/apps/assets/serializers/asset/database.py @@ -1,11 +1,22 @@ - from assets.models import Database from .common import AssetSerializer +from ..gateway import GatewayWithAccountSecretSerializer -__all__ = ['DatabaseSerializer'] +__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer'] class DatabaseSerializer(AssetSerializer): class Meta(AssetSerializer.Meta): model = Database - fields = AssetSerializer.Meta.fields + ['db_name'] + extra_fields = [ + 'db_name', 'use_ssl', 'ca_cert', 'client_cert', + 'client_key', 'allow_invalid_cert' + ] + fields = AssetSerializer.Meta.fields + extra_fields + + +class DatabaseWithGatewaySerializer(DatabaseSerializer): + gateway = GatewayWithAccountSecretSerializer() + + class Meta(DatabaseSerializer.Meta): + fields = DatabaseSerializer.Meta.fields + ['gateway'] diff --git a/apps/assets/serializers/automations/__init__.py b/apps/assets/serializers/automations/__init__.py index e4daeda95..9b5ed21c9 100644 --- a/apps/assets/serializers/automations/__init__.py +++ b/apps/assets/serializers/automations/__init__.py @@ -1,3 +1 @@ from .base import * -from .change_secret import * -from .gather_accounts import * diff --git a/apps/assets/serializers/automations/base.py b/apps/assets/serializers/automations/base.py index 3ff3f6686..fa20c4c28 100644 --- a/apps/assets/serializers/automations/base.py +++ b/apps/assets/serializers/automations/base.py @@ -2,11 +2,11 @@ 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 +from common.const.choices import Trigger +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField logger = get_logger(__file__) @@ -24,10 +24,10 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe 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' - ] + fields = [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', + 'type', 'accounts', 'nodes', 'assets', 'is_active' + ] + read_only_fields extra_kwargs = { 'name': {'required': True}, 'type': {'read_only': True}, @@ -37,15 +37,14 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe 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')) + trigger = LabeledChoiceField(choices=Trigger.choices, label=_("Trigger mode")) class Meta: model = AutomationExecution read_only_fields = [ - 'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status' + 'trigger', 'date_start', 'date_finished', 'snapshot', 'status' ] - fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields + fields = ['id', 'automation', 'trigger'] + read_only_fields @staticmethod def get_snapshot(obj): @@ -57,7 +56,6 @@ class AutomationExecutionSerializer(serializers.ModelSerializer): 'accounts': obj.snapshot['accounts'], 'node_amount': len(obj.snapshot['nodes']), 'asset_amount': len(obj.snapshot['assets']), - 'type_display': getattr(AutomationTypes, tp).label, } return snapshot diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 9641ce786..3d98261b1 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -1,57 +1,3 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers -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): - secret_type = LabeledChoiceField( - choices=SecretType.choices, required=True, label=_('Secret type') - ) - 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') - ) - - @property - def initial_secret_type(self): - secret_type = self.initial_data.get('secret_type') - return secret_type - - 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 ('secret',): - value = validated_data.get(field) - if value is None: - validated_data.pop(field, None) - validated_data.pop('passphrase', None) - - def create(self, validated_data): - self.clean_auth_fields(validated_data) - return super().create(validated_data) - - def update(self, instance, validated_data): - self.clean_auth_fields(validated_data) - return super().update(instance, validated_data) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 8fc711146..6692ea03b 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from common.serializers.fields import ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.drf.fields import ObjectRelatedField -from ..serializers import GatewaySerializer +from .gateway import GatewayWithAccountSecretSerializer from ..models import Domain, Asset - __all__ = ['DomainSerializer', 'DomainWithGatewaySerializer'] @@ -26,11 +26,17 @@ class DomainSerializer(BulkOrgResourceModelSerializer): fields_m2m = ['assets', 'gateways'] read_only_fields = ['date_created'] fields = fields_small + fields_m2m + read_only_fields - extra_kwargs = {} + + def to_representation(self, instance): + data = super().to_representation(instance) + assets = data['assets'] + gateway_ids = [str(i['id']) for i in data['gateways']] + data['assets'] = [i for i in assets if str(i['id']) not in gateway_ids] + return data -class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer): - gateways = GatewaySerializer(many=True, read_only=True) +class DomainWithGatewaySerializer(serializers.ModelSerializer): + gateways = GatewayWithAccountSecretSerializer(many=True, read_only=True) class Meta: model = Domain diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index cc0647943..909c13506 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,10 +4,9 @@ from rest_framework import serializers from orgs.utils import tmp_to_root_org -from common.drf.serializers import BulkSerializerMixin +from common.serializers import BulkSerializerMixin from ..models import FavoriteAsset - __all__ = ['FavoriteAssetSerializer'] diff --git a/apps/assets/serializers/gateway.py b/apps/assets/serializers/gateway.py index 8dab64957..012348719 100644 --- a/apps/assets/serializers/gateway.py +++ b/apps/assets/serializers/gateway.py @@ -1,11 +1,33 @@ # -*- coding: utf-8 -*- # -from ..serializers import HostSerializer -from ..models import Gateway +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers -__all__ = ['GatewaySerializer'] +from .asset import HostSerializer +from .asset.common import AccountSecretSerializer +from ..models import Gateway, Asset + +__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer'] class GatewaySerializer(HostSerializer): class Meta(HostSerializer.Meta): model = Gateway + + def validate_name(self, value): + queryset = Asset.objects.filter(name=value) + if self.instance: + queryset = queryset.exclude(id=self.instance.id) + has = queryset.exists() + if has: + raise serializers.ValidationError( + _('This field must be unique.') + ) + return value + + +class GatewayWithAccountSecretSerializer(GatewaySerializer): + account = AccountSecretSerializer(required=False, label=_('Account'), source='select_account') + + class Meta(GatewaySerializer.Meta): + fields = GatewaySerializer.Meta.fields + ['account'] diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index ab72cecc9..6e622aaf9 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -1,8 +1,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField -from common.drf.serializers import WritableNestedModelSerializer +from common.serializers.fields import LabeledChoiceField +from common.serializers import WritableNestedModelSerializer from ..const import Category, AllTypes from ..models import Platform, PlatformProtocol, PlatformAutomation @@ -44,6 +44,7 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): "id", "ansible_enabled", "ansible_config", "ping_enabled", "ping_method", + "push_account_enabled", "push_account_method", "gather_facts_enabled", "gather_facts_method", "change_secret_enabled", "change_secret_method", "verify_account_enabled", "verify_account_method", @@ -58,6 +59,8 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): "verify_account_method": {"label": "校验账号方式"}, "change_secret_enabled": {"label": "启用账号改密"}, "change_secret_method": {"label": "账号改密方式"}, + "push_account_enabled": {"label": "启用推送账号"}, + "push_account_method": {"label": "推送账号方式"}, "gather_accounts_enabled": {"label": "启用账号收集"}, "gather_accounts_method": {"label": "收集账号方式"}, } @@ -100,18 +103,24 @@ class PlatformSerializer(WritableNestedModelSerializer): "category", "type", "charset", ] fields = fields_small + [ - "protocols_enabled", "protocols", + "protocols", "domain_enabled", "su_enabled", "su_method", "automation", "comment", ] extra_kwargs = { "su_enabled": {"label": "启用切换账号"}, - "protocols_enabled": {"label": "启用协议"}, "domain_enabled": {"label": "启用网域"}, "domain_default": {"label": "默认网域"}, } + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.prefetch_related( + 'protocols', 'automation' + ) + return queryset + class PlatformOpsMethodSerializer(serializers.Serializer): id = serializers.CharField(read_only=True) diff --git a/apps/assets/serializers/utils.py b/apps/assets/serializers/utils.py index 770710843..139597f9c 100644 --- a/apps/assets/serializers/utils.py +++ b/apps/assets/serializers/utils.py @@ -1,25 +1,2 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from common.utils import validate_ssh_private_key, parse_ssh_private_key_str -def validate_password_for_ansible(password): - """ 校验 Ansible 不支持的特殊字符 """ - # validate password contains left double curly bracket - # check password not contains `{{` - # Ansible 推送的时候不支持 - if '{{' in password: - raise serializers.ValidationError(_('Password can not contains `{{` ')) - # Ansible Windows 推送的时候不支持 - if "'" in password: - raise serializers.ValidationError(_("Password can not contains `'` ")) - 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")) - return parse_ssh_private_key_str(ssh_key, passphrase) diff --git a/apps/assets/signal_handlers/__init__.py b/apps/assets/signal_handlers/__init__.py index b337df001..aab861fd3 100644 --- a/apps/assets/signal_handlers/__init__.py +++ b/apps/assets/signal_handlers/__init__.py @@ -1,4 +1,3 @@ from .asset import * -from .account import * from .node_assets_amount import * from .node_assets_mapping import * diff --git a/apps/assets/signal_handlers/account.py b/apps/assets/signal_handlers/account.py deleted file mode 100644 index 8020e4087..000000000 --- a/apps/assets/signal_handlers/account.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.dispatch import receiver -from django.db.models.signals import pre_save - -from common.utils import get_logger -from ..models import Account - -logger = get_logger(__name__) - - -@receiver(pre_save, sender=Account) -def on_account_pre_create(sender, instance, **kwargs): - # 升级版本号 - instance.version += 1 - # 即使在 root 组织也不怕 - instance.org_id = instance.asset.org_id diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py index 86e6a0a9a..55cd0ede2 100644 --- a/apps/assets/signal_handlers/asset.py +++ b/apps/assets/signal_handlers/asset.py @@ -1,32 +1,16 @@ # -*- coding: utf-8 -*- # -from django.db.models.signals import ( - post_save, m2m_changed, pre_delete, post_delete, pre_save -) +from django.db.models.signals import post_save, m2m_changed, pre_delete, post_delete, pre_save from django.dispatch import receiver +from assets.models import Asset, Node, Cloud, Device, Host, Web, Database from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE -from common.utils import get_logger from common.decorator import on_transaction_commit -from assets.models import Asset, Node -from assets.tasks import ( - update_assets_hardware_info_util, - test_asset_connectivity_util, -) +from common.utils import get_logger logger = get_logger(__file__) -def update_asset_hardware_info_on_created(asset): - logger.debug("Update asset `{}` hardware info".format(asset)) - update_assets_hardware_info_util.delay([asset]) - - -def test_asset_conn_on_created(asset): - logger.debug("Test asset `{}` connectivity".format(asset)) - test_asset_connectivity_util.delay([asset]) - - @receiver(pre_save, sender=Node) def on_node_pre_save(sender, instance: Node, **kwargs): instance.parent_key = instance.compute_parent_key() @@ -34,22 +18,23 @@ def on_node_pre_save(sender, instance: Node, **kwargs): @receiver(post_save, sender=Asset) @on_transaction_commit -def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): +def on_asset_create(sender, instance=None, created=False, **kwargs): """ 当资产创建时,更新硬件信息,更新可连接性 确保资产必须属于一个节点 """ - if created: - logger.info("Asset create signal recv: {}".format(instance)) + if not created: + return + logger.info("Asset create signal recv: {}".format(instance)) - # 获取资产硬件信息 - update_asset_hardware_info_on_created(instance) - test_asset_conn_on_created(instance) + # 获取资产硬件信息 + # update_assets_fact_util.delay([instance]) + # test_asset_connectivity_util.delay([instance]) - # 确保资产存在一个节点 - has_node = instance.nodes.all().exists() - if not has_node: - instance.nodes.add(Node.org_root()) + # 确保资产存在一个节点 + has_node = instance.nodes.all().exists() + if not has_node: + instance.nodes.add(Node.org_root()) @receiver(m2m_changed, sender=Asset.nodes.through) @@ -109,6 +94,7 @@ RELATED_NODE_IDS = '_related_node_ids' @receiver(pre_delete, sender=Asset) def on_asset_delete(instance: Asset, using, **kwargs): + logger.debug("Asset pre delete signal recv: {}".format(instance)) node_ids = set(Node.objects.filter( assets=instance ).distinct().values_list('id', flat=True)) @@ -121,9 +107,19 @@ def on_asset_delete(instance: Asset, using, **kwargs): @receiver(post_delete, sender=Asset) def on_asset_post_delete(instance: Asset, using, **kwargs): + logger.debug("Asset delete signal recv: {}".format(instance)) node_ids = getattr(instance, RELATED_NODE_IDS, None) if node_ids: m2m_changed.send( sender=Asset.nodes.through, instance=instance, reverse=False, model=Node, pk_set=node_ids, using=using, action=POST_REMOVE ) + + +def resend_to_asset_signals(sender, signal, **kwargs): + signal.send(sender=Asset, **kwargs) + + +for model in (Host, Database, Device, Web, Cloud): + for s in (pre_save, post_save): + s.connect(resend_to_asset_signals, sender=model) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index 060f4d2d9..cf2ccd2cd 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -3,10 +3,6 @@ from .ping import * from .utils import * from .common import * -from .backup import * from .automation import * from .gather_facts import * from .nodes_amount import * -from .push_account import * -from .verify_account import * -from .gather_accounts import * diff --git a/apps/assets/tasks/automation.py b/apps/assets/tasks/automation.py index 60f01836f..582fe7ea2 100644 --- a/apps/assets/tasks/automation.py +++ b/apps/assets/tasks/automation.py @@ -8,7 +8,7 @@ from assets.const import AutomationTypes logger = get_logger(__file__) -@shared_task(queue='ansible', verbose_name=_('Execute automation')) +@shared_task(queue='ansible', verbose_name=_('Asset execute automation')) def execute_automation(pid, trigger, tp): model = AutomationTypes.get_type_model(tp) with tmp_to_root_org(): diff --git a/apps/assets/tasks/common.py b/apps/assets/tasks/common.py index ec51c5a2b..9bafbcde8 100644 --- a/apps/assets/tasks/common.py +++ b/apps/assets/tasks/common.py @@ -1,2 +1,46 @@ # -*- coding: utf-8 -*- # +import uuid +from celery import current_task +from django.db.utils import IntegrityError +from orgs.utils import current_org + +from common.const.choices import Trigger + + +def generate_data(task_name, tp, child_snapshot=None): + child_snapshot = child_snapshot or {} + from assets.models import BaseAutomation + try: + eid = current_task.request.id + except AttributeError: + eid = str(uuid.uuid4()) + + data = { + 'type': tp, + 'name': task_name, + 'org_id': str(current_org.id) + } + + automation_instance = BaseAutomation() + snapshot = automation_instance.to_attr_json() + snapshot.update(data) + snapshot.update(child_snapshot) + return {'id': eid, 'snapshot': snapshot} + + +def automation_execute_start(task_name, tp, child_snapshot=None): + from assets.models import AutomationExecution + data = generate_data(task_name, tp, child_snapshot) + + while True: + try: + _id = data['id'] + AutomationExecution.objects.get(id=_id) + data['id'] = str(uuid.uuid4()) + except AutomationExecution.DoesNotExist: + break + execution = AutomationExecution.objects.create( + trigger=Trigger.manual, **data + ) + execution.start() diff --git a/apps/assets/tasks/gather_facts.py b/apps/assets/tasks/gather_facts.py index b3196abf5..ad678b92a 100644 --- a/apps/assets/tasks/gather_facts.py +++ b/apps/assets/tasks/gather_facts.py @@ -1,60 +1,66 @@ # -*- coding: utf-8 -*- # from celery import shared_task -from django.utils.translation import gettext_noop -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_noop, gettext_lazy as _ from common.utils import get_logger +from assets.const import AutomationTypes from orgs.utils import org_aware_func, tmp_to_root_org +from .common import automation_execute_start + logger = get_logger(__file__) __all__ = [ - 'update_assets_hardware_info_util', + 'update_assets_fact_util', 'update_node_assets_hardware_info_manual', 'update_assets_hardware_info_manual', ] -@org_aware_func('assets') -def update_assets_hardware_info_util(assets=None, nodes=None, task_name=None): +def update_fact_util(assets=None, nodes=None, task_name=None): from assets.models import GatherFactsAutomation - if not assets and not nodes: - logger.info("No assets or nodes to update hardware info") - return - if task_name is None: task_name = gettext_noop("Update some assets hardware info. ") task_name = GatherFactsAutomation.generate_unique_name(task_name) - comment = '' - if assets: - comment += 'asset:' + ', '.join([str(i) for i in assets]) + '\n' - if nodes: - comment += 'node:' + ', '.join([str(i) for i in nodes]) - data = {'name': task_name, 'comment': comment} - instance = GatherFactsAutomation.objects.create(**data) + nodes = nodes or [] + assets = assets or [] + child_snapshot = { + 'assets': [str(asset.id) for asset in assets], + 'nodes': [str(node.id) for node in nodes], + } + tp = AutomationTypes.gather_facts + automation_execute_start(task_name, tp, child_snapshot) - if assets: - instance.assets.add(*assets) - if nodes: - instance.nodes.add(*nodes) - instance.execute() + +@org_aware_func('assets') +def update_assets_fact_util(assets=None, task_name=None): + if assets is None: + logger.info("No assets to update hardware info") + return + + update_fact_util(assets=assets, task_name=task_name) + + +@org_aware_func('nodes') +def update_nodes_fact_util(nodes=None, task_name=None): + if nodes is None: + logger.info("No nodes to update hardware info") + return + update_fact_util(nodes=nodes, task_name=task_name) @shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets')) def update_assets_hardware_info_manual(asset_ids): from assets.models import Asset - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) + assets = Asset.objects.filter(id__in=asset_ids) task_name = gettext_noop("Update assets hardware info: ") - update_assets_hardware_info_util(assets=assets, task_name=task_name) + update_assets_fact_util(assets=assets, task_name=task_name) @shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets under a node')) def update_node_assets_hardware_info_manual(node_id): from assets.models import Node - with tmp_to_root_org(): - node = Node.objects.get(id=node_id) - + node = Node.objects.get(id=node_id) task_name = gettext_noop("Update node asset hardware information: ") - update_assets_hardware_info_util(nodes=[node], task_name=task_name) + update_nodes_fact_util(nodes=[node], task_name=task_name) diff --git a/apps/assets/tasks/ping.py b/apps/assets/tasks/ping.py index 817f64b64..c0b53a9c8 100644 --- a/apps/assets/tasks/ping.py +++ b/apps/assets/tasks/ping.py @@ -1,12 +1,15 @@ # ~*~ coding: utf-8 ~*~ from celery import shared_task -from django.utils.translation import gettext_noop -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_noop, gettext_lazy as _ from common.utils import get_logger -from orgs.utils import org_aware_func, tmp_to_root_org +from assets.const import AutomationTypes, GATEWAY_NAME +from orgs.utils import org_aware_func + +from .common import automation_execute_start logger = get_logger(__file__) + __all__ = [ 'test_asset_connectivity_util', 'test_assets_connectivity_manual', @@ -14,38 +17,48 @@ __all__ = [ ] +def test_connectivity_util(assets, tp, task_name=None, local_port=None): + if not assets: + return + + if local_port is None: + child_snapshot = {} + else: + child_snapshot = {'local_port': local_port} + + child_snapshot['assets'] = [str(asset.id) for asset in assets] + automation_execute_start(task_name, tp, child_snapshot) + + @org_aware_func('assets') -def test_asset_connectivity_util(assets, task_name=None): +def test_asset_connectivity_util(assets, task_name=None, local_port=None): from assets.models import PingAutomation if task_name is None: task_name = gettext_noop("Test assets connectivity ") task_name = PingAutomation.generate_unique_name(task_name) - data = { - 'name': task_name, - 'comment': ', '.join([str(i) for i in assets]) - } - instance = PingAutomation.objects.create(**data) - instance.assets.add(*assets) - instance.execute() + + gateway_assets = assets.filter(platform__name=GATEWAY_NAME) + test_connectivity_util( + gateway_assets, AutomationTypes.ping_gateway, task_name, local_port + ) + + non_gateway_assets = assets.exclude(platform__name=GATEWAY_NAME) + test_connectivity_util(non_gateway_assets, AutomationTypes.ping, task_name) -@shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of a asset')) -def test_assets_connectivity_manual(asset_ids): +@shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of a asset')) +def test_assets_connectivity_manual(asset_ids, local_port=None): from assets.models import Asset - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) - + assets = Asset.objects.filter(id__in=asset_ids) task_name = gettext_noop("Test assets connectivity ") - test_asset_connectivity_util(assets, task_name=task_name) + test_asset_connectivity_util(assets, task_name, local_port) @shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of assets under a node')) -def test_node_assets_connectivity_manual(node_id): +def test_node_assets_connectivity_manual(node_id, local_port=None): from assets.models import Node - with tmp_to_root_org(): - node = Node.objects.get(id=node_id) - + node = Node.objects.get(id=node_id) task_name = gettext_noop("Test if the assets under the node are connectable ") assets = node.get_all_assets() - test_asset_connectivity_util(assets, task_name=task_name) + test_asset_connectivity_util(assets, task_name, local_port) diff --git a/apps/assets/tasks/push_account.py b/apps/assets/tasks/push_account.py deleted file mode 100644 index 7c596c8f2..000000000 --- a/apps/assets/tasks/push_account.py +++ /dev/null @@ -1,41 +0,0 @@ -from celery import shared_task -from django.utils.translation import gettext_noop - -from common.utils import get_logger -from orgs.utils import org_aware_func, tmp_to_root_org -from django.utils.translation import ugettext_lazy as _ - -logger = get_logger(__file__) -__all__ = [ - 'push_accounts_to_assets', -] - - -@org_aware_func("assets") -def push_accounts_to_assets_util(accounts, assets, username=None): - from assets.models import PushAccountAutomation - task_name = gettext_noop("Push accounts to assets") - task_name = PushAccountAutomation.generate_unique_name(task_name) - if username is None: - account_usernames = list(accounts.values_list('username', flat=True)) - else: - account_usernames = [username] - - data = { - 'name': task_name, - 'accounts': account_usernames, - 'comment': ', '.join([str(i) for i in assets]) - } - instance = PushAccountAutomation.objects.create(**data) - instance.assets.add(*assets) - instance.execute() - - -@shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) -def push_accounts_to_assets(account_ids, asset_ids, username=None): - from assets.models import Asset, Account - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) - accounts = Account.objects.filter(id__in=account_ids) - - return push_accounts_to_assets_util(accounts, assets, username) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index dd42c44eb..a9a385eb9 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -14,24 +14,12 @@ router.register(r'devices', api.DeviceViewSet, 'device') router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'webs', api.WebViewSet, 'web') router.register(r'clouds', api.CloudViewSet, 'cloud') -router.register(r'accounts', api.AccountViewSet, 'account') -router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') -router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') -router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') 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'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') -router.register(r'gather-account-executions', api.GatherAccountsExecutionViewSet, 'gather-account-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//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), @@ -45,10 +33,6 @@ urlpatterns = [ path('assets//perm-user-groups//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//histories/', api.AccountHistoriesSecretAPI.as_view(), - name='account-secret-history'), - path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'), path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'), path('nodes//children/', api.NodeChildrenApi.as_view(), name='node-children'), @@ -61,11 +45,6 @@ urlpatterns = [ path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), - - path('automation//asset/remove/', api.AutomationRemoveAssetApi.as_view(), name='automation-remove-asset'), - path('automation//asset/add/', api.AutomationAddAssetApi.as_view(), name='automation-add-asset'), - path('automation//nodes/', api.AutomationNodeAddRemoveApi.as_view(), name='automation-add-or-remove-node'), - path('automation//assets/', api.AutomationAssetsListApi.as_view(), name='automation-assets'), ] urlpatterns += router.urls diff --git a/apps/assets/utils/node.py b/apps/assets/utils/node.py index 5d22421c5..deaa7d32b 100644 --- a/apps/assets/utils/node.py +++ b/apps/assets/utils/node.py @@ -2,7 +2,7 @@ # from collections import defaultdict from common.utils import get_logger, dict_get_any, is_uuid, get_object_or_none, timeit -from common.http import is_true +from common.utils.http import is_true from common.struct import Stack from common.db.models import output_as_string from orgs.utils import ensure_in_real_or_default_org, current_org diff --git a/apps/audits/api.py b/apps/audits/api.py index b060a0eb1..383b0d45b 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -8,16 +8,17 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin from ops.models.job import JobAuditLog -from common.api import CommonGenericViewSet +from common.api import JMSGenericViewSet from common.drf.filters import DatetimeRangeFilter from common.plugins.es import QuerySet as ESQuerySet -from orgs.utils import current_org +from orgs.utils import current_org, tmp_to_root_org from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet from .backends import TYPE_ENGINE_MAPPING from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog from .serializers import FTPLogSerializer, UserLoginLogSerializer, JobAuditLogSerializer from .serializers import ( - OperateLogSerializer, OperateLogActionDetailSerializer, PasswordChangeLogSerializer + OperateLogSerializer, OperateLogActionDetailSerializer, + PasswordChangeLogSerializer, ActivitiesOperatorLogSerializer, ) @@ -50,7 +51,7 @@ class UserLoginCommonMixin: search_fields = ['username', 'ip', 'city'] -class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, CommonGenericViewSet): +class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, JMSGenericViewSet): @staticmethod def get_org_members(): @@ -75,6 +76,19 @@ class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView): return qs +class ResourceActivityAPIView(generics.ListAPIView): + serializer_class = ActivitiesOperatorLogSerializer + rbac_perms = { + 'GET': 'audits.view_operatelog', + } + + def get_queryset(self): + resource_id = self.request.query_params.get('resource_id') + with tmp_to_root_org(): + queryset = OperateLog.objects.filter(resource_id=resource_id)[:30] + return queryset + + class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): model = OperateLog serializer_class = OperateLogSerializer @@ -92,7 +106,7 @@ class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): return super().get_serializer_class() def get_queryset(self): - qs = OperateLog.objects.all() + qs = OperateLog.objects.filter(is_activity=False) es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG if es_config: engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) @@ -103,7 +117,7 @@ class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): return qs -class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): +class PasswordChangeLogViewSet(ListModelMixin, JMSGenericViewSet): queryset = PasswordChangeLog.objects.all() serializer_class = PasswordChangeLogSerializer extra_filter_backends = [DatetimeRangeFilter] diff --git a/apps/audits/backends/db.py b/apps/audits/backends/db.py index 14222eb13..a3d941f8e 100644 --- a/apps/audits/backends/db.py +++ b/apps/audits/backends/db.py @@ -17,22 +17,26 @@ class OperateLogStore(object): return True def save(self, **kwargs): + before_limit, after_limit = None, None log_id = kwargs.get('id', '') before = kwargs.get('before') or {} after = kwargs.get('after') or {} if len(str(before)) > self.max_length: - before = {_('Tips'): self.max_length_tip_msg} + before_limit = {str(_('Tips')): self.max_length_tip_msg} if len(str(after)) > self.max_length: - after = {_('Tips'): self.max_length_tip_msg} + after_limit = {str(_('Tips')): self.max_length_tip_msg} op_log = self.model.objects.filter(pk=log_id).first() if op_log is not None: - raw_after = op_log.after or {} - raw_before = op_log.before or {} - raw_before.update(before) - raw_after.update(after) - op_log.before = raw_before - op_log.after = raw_after - op_log.save() + op_log_before = op_log.before or {} + op_log_after = op_log.after or {} + if not before_limit: + before.update(op_log_before) + if not after_limit: + after.update(op_log_after) else: - self.model.objects.create(**kwargs) + op_log = self.model(**kwargs) + + op_log.before = before_limit if before_limit else before + op_log.after = after_limit if after_limit else after + op_log.save() diff --git a/apps/audits/const.py b/apps/audits/const.py index 6987de94f..1dc37de47 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -5,37 +5,7 @@ from django.db.models import TextChoices, IntegerChoices DEFAULT_CITY = _("Unknown") -MODELS_NEED_RECORD = ( - # users - 'User', 'UserGroup', - # authentication - 'AccessKey', 'TempToken', - "User", - "UserGroup", - # acls - "LoginACL", - "LoginAssetACL", - "LoginConfirmSetting", - # assets - 'Asset', 'Node', 'Domain', 'Gateway', 'CommandFilterRule', - 'CommandFilter', 'Platform', 'Label', - # account - 'Account', - # orgs - "Organization", - # settings - "Setting", - # perms - 'AssetPermission', - # notifications - 'SystemMsgSubscription', 'UserMsgSubscription', - # Terminal - 'Terminal', 'Endpoint', 'EndpointRule', 'CommandStorage', 'ReplayStorage', - # rbac - 'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding', - # xpack - 'License', 'Account', 'SyncInstanceTask', 'Interface', -) +MODELS_NEED_RECORD = set() class OperateChoices(TextChoices): @@ -53,6 +23,10 @@ class ActionChoices(TextChoices): update = "update", _("Update") delete = "delete", _("Delete") create = "create", _("Create") + # Activities action + connect = "connect", _("Connect") + login = "login", _("Login") + change_auth = "change_password", _("Change password") class LoginTypeChoices(TextChoices): diff --git a/apps/audits/handler.py b/apps/audits/handler.py index 8df3c1f37..13ea6f16a 100644 --- a/apps/audits/handler.py +++ b/apps/audits/handler.py @@ -4,6 +4,7 @@ from django.db import transaction from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from users.models import User from common.utils import get_request_ip, get_logger from common.utils.timezone import as_current_tz from common.utils.encode import Singleton @@ -14,6 +15,7 @@ from audits.models import OperateLog from orgs.utils import get_current_org_id from .backends import get_operate_log_storage +from .const import ActionChoices logger = get_logger(__name__) @@ -81,7 +83,7 @@ class OperatorLogHandler(metaclass=Singleton): def get_instance_dict_from_cache(self, instance_id): if instance_id is None: - return None + return None, None key = '%s_%s' % (self.CACHE_KEY, instance_id) cache_instance = cache.get(key, {}) @@ -148,9 +150,49 @@ class OperatorLogHandler(metaclass=Singleton): after = self.__data_processing(after) return before, after + @staticmethod + def _get_Session_params(resource, **kwargs): + # 更新会话的日志不在Activity中体现, + # 否则会话结束,录像文件结束操作的会话记录都会体现出来 + params = {} + action = kwargs.get('data', {}).get('action', 'create') + if action == ActionChoices.create: + params = { + 'action': ActionChoices.connect, + 'resource_id': str(resource.asset_id), + 'user': resource.user + } + return params + + @staticmethod + def _get_ChangeSecretRecord_params(resource, **kwargs): + return { + 'action': ActionChoices.change_auth, + 'resource_id': str(resource.account_id), + } + + @staticmethod + def _get_UserLoginLog_params(resource, **kwargs): + username = resource.username + user_id = User.objects.filter(username=username).\ + values_list('id', flat=True)[0] + return { + 'action': ActionChoices.login, + 'resource_id': str(user_id), + } + + def _activity_handle(self, data, object_name, resource): + param_func = getattr(self, '_get_%s_params' % object_name, None) + if param_func is not None: + params = param_func(resource, data=data) + data['is_activity'] = True + data.update(params) + return data + def create_or_update_operate_log( self, action, resource_type, resource=None, - force=False, log_id=None, before=None, after=None + force=False, log_id=None, before=None, after=None, + object_name=None ): user = current_request.user if current_request else None if not user or not user.is_authenticated: @@ -167,8 +209,9 @@ class OperatorLogHandler(metaclass=Singleton): 'id': log_id, "user": str(user), 'action': action, 'resource_type': str(resource_type), 'resource': resource_display, 'remote_addr': remote_addr, 'before': before, 'after': after, - 'org_id': get_current_org_id(), + 'org_id': get_current_org_id(), 'resource_id': str(resource.id) } + data = self._activity_handle(data, object_name, resource=resource) with transaction.atomic(): if self.log_client.ping(timeout=1): client = self.log_client diff --git a/apps/audits/migrations/0016_auto_20221111_1919.py b/apps/audits/migrations/0016_auto_20221111_1919.py index fb546c83f..226588e7b 100644 --- a/apps/audits/migrations/0016_auto_20221111_1919.py +++ b/apps/audits/migrations/0016_auto_20221111_1919.py @@ -20,9 +20,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='operatelog', name='action', - field=models.CharField( - choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create')], - max_length=16, verbose_name='Action'), + field=models.CharField(choices=[ + ('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), + ('create', 'Create'), ('connect', 'Connect'), ('login', 'Login'), + ('change_password', 'Change password') + ], max_length=16, verbose_name='Action'), ), migrations.AlterField( model_name='userloginlog', diff --git a/apps/audits/migrations/0018_operatelog_resource_id.py b/apps/audits/migrations/0018_operatelog_resource_id.py new file mode 100644 index 000000000..44953cae3 --- /dev/null +++ b/apps/audits/migrations/0018_operatelog_resource_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2023-01-03 08:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0017_auto_20221220_1757'), + ] + + operations = [ + migrations.AddField( + model_name='operatelog', + name='resource_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Resource'), + ), + ] diff --git a/apps/audits/migrations/0019_alter_operatelog_options.py b/apps/audits/migrations/0019_alter_operatelog_options.py new file mode 100644 index 000000000..c6ebcb76c --- /dev/null +++ b/apps/audits/migrations/0019_alter_operatelog_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0018_operatelog_resource_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='operatelog', + options={'ordering': ('-datetime',), 'verbose_name': 'Operate log'}, + ), + ] diff --git a/apps/terminal/migrations/0063_applet_builtin.py b/apps/audits/migrations/0020_auto_20230112_1034.py similarity index 56% rename from apps/terminal/migrations/0063_applet_builtin.py rename to apps/audits/migrations/0020_auto_20230112_1034.py index 1d991e180..d3dd634fa 100644 --- a/apps/terminal/migrations/0063_applet_builtin.py +++ b/apps/audits/migrations/0020_auto_20230112_1034.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.14 on 2022-12-20 07:24 +# Generated by Django 3.2.14 on 2023-01-12 02:34 from django.db import migrations, models @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('terminal', '0062_auto_20221216_1529'), + ('audits', '0019_alter_operatelog_options'), ] operations = [ migrations.AddField( - model_name='applet', - name='builtin', - field=models.BooleanField(default=False, verbose_name='Builtin'), + model_name='operatelog', + name='is_activity', + field=models.BooleanField(default=False, verbose_name='Is Activity'), ), ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 5f11fca9f..370a9513f 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -52,10 +52,15 @@ class OperateLog(OrgModelMixin): ) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) + resource_id = models.CharField( + max_length=36, blank=True, default='', db_index=True, + verbose_name=_("Resource") + ) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True) before = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True) after = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True) + is_activity = models.BooleanField(default=False, verbose_name=(_('Is Activity'))) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) @@ -86,6 +91,7 @@ class OperateLog(OrgModelMixin): class Meta: verbose_name = _("Operate log") + ordering = ('-datetime',) class PasswordChangeLog(models.Model): diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index f5aee2f65..8fd8a10dc 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -3,7 +3,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField +from common.utils.timezone import as_current_tz from ops.models.job import JobAuditLog from ops.serializers.job import JobExecutionSerializer from terminal.models import Session @@ -22,6 +23,7 @@ class JobAuditLogSerializer(JobExecutionSerializer): "id", "material", "time_cost", 'date_start', 'date_finished', 'date_created', 'is_finished', 'is_success', 'created_by', + 'task_id' ] fields = read_only_fields + [] @@ -94,3 +96,18 @@ class SessionAuditSerializer(serializers.ModelSerializer): class Meta: model = Session fields = "__all__" + + +class ActivitiesOperatorLogSerializer(serializers.Serializer): + timestamp = serializers.SerializerMethodField() + content = serializers.SerializerMethodField() + + @staticmethod + def get_timestamp(obj): + return as_current_tz(obj.datetime).strftime('%Y-%m-%d %H:%M:%S') + + @staticmethod + def get_content(obj): + action = obj.action.replace('_', ' ').capitalize() + ctn = _('User {} {} this resource.').format(obj.user, _(action)) + return ctn diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index 546fea60a..a7649bbe5 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -2,6 +2,7 @@ # import uuid +from django.apps import apps from django.conf import settings from django.contrib.auth import BACKEND_SESSION_KEY from django.db import transaction @@ -21,6 +22,7 @@ from audits.utils import model_to_dict_for_operate_log as model_to_dict from authentication.signals import post_auth_failed, post_auth_success from authentication.utils import check_different_city_login_if_need from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL +from common.signals import django_ready from common.utils import get_request_ip, get_logger, get_syslogger from common.utils.encode import data_to_json from jumpserver.utils import current_request @@ -164,9 +166,10 @@ def on_object_created_or_update(sender, instance=None, created=False, update_fie log_id, before, after = get_instance_current_with_cache_diff(current_instance) resource_type = sender._meta.verbose_name + object_name = sender._meta.object_name create_or_update_operate_log( - action, resource_type, resource=instance, - log_id=log_id, before=before, after=after + action, resource_type, resource=instance, log_id=log_id, + before=before, after=after, object_name=object_name ) @@ -278,3 +281,34 @@ def on_user_auth_failed(sender, username, request, reason='', **kwargs): data = generate_data(username, request) data.update({'reason': reason[:128], 'status': False}) write_login_log(**data) + + +@receiver(django_ready) +def on_django_start_set_operate_log_monitor_models(sender, **kwargs): + exclude_label = { + 'django_cas_ng', 'captcha', 'admin', 'jms_oidc_rp', + 'django_celery_beat', 'contenttypes', 'sessions', 'auth' + } + exclude_object_name = { + 'UserPasswordHistory', 'ContentType', + 'SiteMessage', 'SiteMessageUsers', + 'PlatformAutomation', 'PlatformProtocol', 'Protocol', + 'HistoricalAccount', 'GatheredUser', 'ApprovalRule', + 'BaseAutomation', 'CeleryTask', 'Command', 'JobAuditLog', + 'ConnectionToken', 'SessionJoinRecord', + 'HistoricalJob', 'Status', 'TicketStep', 'Ticket', + 'UserAssetGrantedTreeNodeRelation', 'TicketAssignee', + 'SuperTicket', 'SuperConnectionToken', 'PermNode', + 'PermedAsset', 'PermedAccount', 'MenuPermission', + 'Permission', 'TicketSession', 'ApplyLoginTicket', + 'ApplyCommandTicket', 'ApplyLoginAssetTicket', + 'FTPLog', 'OperateLog', 'PasswordChangeLog' + } + for i, app in enumerate(apps.get_models(), 1): + app_label = app._meta.app_label + app_object_name = app._meta.object_name + if app_label in exclude_label or \ + app_object_name in exclude_object_name or \ + app_object_name.endswith('Execution'): + continue + MODELS_NEED_RECORD.add(app_object_name) diff --git a/apps/audits/urls/api_urls.py b/apps/audits/urls/api_urls.py index fa8ee63fc..96b89e52b 100644 --- a/apps/audits/urls/api_urls.py +++ b/apps/audits/urls/api_urls.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.urls.conf import re_path, path from rest_framework.routers import DefaultRouter -from common import api as capi from .. import api app_name = "audits" @@ -18,10 +17,7 @@ router.register(r'job-logs', api.JobAuditViewSet, 'job-log') urlpatterns = [ path('my-login-logs/', api.MyLoginLogAPIView.as_view(), name='my-login-log'), -] - -old_version_urlpatterns = [ - re_path('(?Pftp-log)/.*', capi.redirect_plural_name_api) + path('activities/', api.ResourceActivityAPIView.as_view(), name='resource-activities'), ] urlpatterns += router.urls diff --git a/apps/audits/utils.py b/apps/audits/utils.py index e62a40577..6f8f9e730 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -56,33 +56,56 @@ def get_resource_display(resource): return resource_display +def _get_instance_field_value( + instance, include_model_fields, + model_need_continue_fields, exclude_fields=None +): + data = {} + opts = getattr(instance, '_meta', None) + if opts is not None: + for f in chain(opts.concrete_fields, opts.private_fields): + if not include_model_fields and not getattr(f, 'primary_key', False): + continue + + if isinstance(f, (models.FileField, models.ImageField)): + continue + + if getattr(f, 'attname', None) in model_need_continue_fields: + continue + + value = getattr(instance, f.name) or getattr(instance, f.attname) + if not isinstance(value, bool) and not value: + continue + + if getattr(f, 'primary_key', False): + f.verbose_name = 'id' + elif isinstance(value, list): + value = [str(v) for v in value] + elif isinstance(f, models.OneToOneField) and isinstance(value, models.Model): + nested_data = _get_instance_field_value( + value, include_model_fields, model_need_continue_fields, ('id',) + ) + for k, v in nested_data.items(): + if exclude_fields and k in exclude_fields: + continue + data.setdefault(k, v) + continue + data.setdefault(str(f.verbose_name), value) + return data + + def model_to_dict_for_operate_log( instance, include_model_fields=True, include_related_fields=True ): model_need_continue_fields = ['date_updated'] m2m_need_continue_fields = ['history_passwords'] - opts = instance._meta - data = {} - for f in chain(opts.concrete_fields, opts.private_fields): - if isinstance(f, (models.FileField, models.ImageField)): - continue - if getattr(f, 'attname', None) in model_need_continue_fields: - continue - - value = getattr(instance, f.name) or getattr(instance, f.attname) - if not isinstance(value, bool) and not value: - continue - - if getattr(f, 'primary_key', False): - f.verbose_name = 'id' - elif isinstance(value, list): - value = [str(v) for v in value] - - if include_model_fields or getattr(f, 'primary_key', False): - data[str(f.verbose_name)] = value + data = _get_instance_field_value( + instance, include_model_fields, model_need_continue_fields + ) if include_related_fields: + opts = instance._meta for f in chain(opts.many_to_many, opts.related_objects): value = [] if instance.pk is not None: @@ -97,7 +120,7 @@ def model_to_dict_for_operate_log( continue try: field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name - data[str(field_key)] = value + data.setdefault(str(field_key), value) except: pass return data diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 782346fc5..ab2783b97 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -6,6 +6,7 @@ import urllib.parse from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -13,10 +14,11 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ValidationError -from common.drf.api import JMSModelViewSet -from common.http import is_true +from common.api import JMSModelViewSet +from common.utils.http import is_true from common.utils import random_string from common.utils.django import get_request_os +from common.exceptions import JMSException from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil @@ -158,7 +160,9 @@ class RDPFileClientProtocolURLMixin: def get_smart_endpoint(self, protocol, asset=None): target_ip = asset.get_target_ip() if asset else '' - endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request) + endpoint = EndpointRule.match_endpoint( + target_instance=asset, target_ip=target_ip, protocol=protocol, request=self.request + ) return endpoint @@ -231,8 +235,6 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return super().perform_create(serializer) def validate_serializer(self, serializer): - from perms.utils.account import PermAccountUtil - data = serializer.validated_data user = self.get_user(serializer) asset = data.get('asset') @@ -241,23 +243,51 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView data['user'] = user data['value'] = random_string(16) - util = PermAccountUtil() - permed_account = util.validate_permission(user, asset, account_name) - - if not permed_account or not permed_account.actions: - msg = 'user `{}` not has asset `{}` permission for account `{}`'.format( - user, asset, account_name - ) - raise PermissionDenied(msg) - - if permed_account.date_expired < timezone.now(): - raise PermissionDenied('Expired') - - if permed_account.has_secret: + account = self._validate_perm(user, asset, account_name) + if account.has_secret: data['input_secret'] = '' - if permed_account.username != '@INPUT': + if account.username != '@INPUT': data['input_username'] = '' - return permed_account + + ticket = self._validate_acl(user, asset, account) + if ticket: + data['from_ticket'] = ticket + data['is_active'] = False + + return account + + @staticmethod + def _validate_perm(user, asset, account_name): + from perms.utils.account import PermAccountUtil + account = PermAccountUtil().validate_permission(user, asset, account_name) + if not account or not account.actions: + msg = _('Account not found') + raise JMSException(code='perm_account_invalid', detail=msg) + if account.date_expired < timezone.now(): + msg = _('Permission Expired') + raise JMSException(code='perm_expired', detail=msg) + return account + + def _validate_acl(self, user, asset, account): + from acls.models import LoginAssetACL + acl = LoginAssetACL.filter_queryset(user, asset, account).valid().first() + if not acl: + return + if acl.is_action(acl.ActionChoices.accept): + return + if acl.is_action(acl.ActionChoices.reject): + msg = _('ACL action is reject') + raise JMSException(code='acl_reject', detail=msg) + if acl.is_action(acl.ActionChoices.review): + if not self.request.query_params.get('create_ticket'): + msg = _('ACL action is review') + raise JMSException(code='acl_review', detail=msg) + + ticket = LoginAssetACL.create_login_asset_review_ticket( + user=user, asset=asset, account_username=account.username, + assignees=acl.reviewers.all(), org_id=asset.org_id + ) + return ticket class SuperConnectionTokenViewSet(ConnectionTokenViewSet): diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index 817b5276e..ee2bdae2e 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from users.models import User from common.utils import get_logger from common.permissions import UserConfirmation -from common.mixins.api import RoleUserMixin, RoleAdminMixin +from common.api import RoleUserMixin, RoleAdminMixin from authentication.const import ConfirmType from authentication import errors diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py index 878ec6e2d..5a6d3721e 100644 --- a/apps/authentication/api/feishu.py +++ b/apps/authentication/api/feishu.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from users.models import User from common.utils import get_logger from common.permissions import UserConfirmation -from common.mixins.api import RoleUserMixin, RoleAdminMixin +from common.api import RoleUserMixin, RoleAdminMixin from authentication.const import ConfirmType from authentication import errors diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py index 88d9707db..c3b2d688c 100644 --- a/apps/authentication/api/sso.py +++ b/apps/authentication/api/sso.py @@ -11,8 +11,8 @@ from rest_framework.permissions import AllowAny from common.utils.timezone import utc_now from common.const.http import POST, GET -from common.drf.api import JMSGenericViewSet -from common.drf.serializers import EmptySerializer +from common.api import JMSGenericViewSet +from common.serializers import EmptySerializer from common.permissions import OnlySuperUser from common.utils import reverse from users.models import User diff --git a/apps/authentication/api/temp_token.py b/apps/authentication/api/temp_token.py index 2fa5791e3..bf8aaa413 100644 --- a/apps/authentication/api/temp_token.py +++ b/apps/authentication/api/temp_token.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.decorators import action from rbac.permissions import RBACPermission -from common.drf.api import JMSModelViewSet +from common.api import JMSModelViewSet from ..models import TempToken from ..serializers import TempTokenSerializer diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index cdde00bc9..2779cc9eb 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from users.models import User from common.utils import get_logger from common.permissions import UserConfirmation -from common.mixins.api import RoleUserMixin, RoleAdminMixin +from common.api import RoleUserMixin, RoleAdminMixin from authentication.const import ConfirmType from authentication import errors diff --git a/apps/authentication/migrations/0017_auto_20230105_1743.py b/apps/authentication/migrations/0017_auto_20230105_1743.py new file mode 100644 index 000000000..f4f9bff69 --- /dev/null +++ b/apps/authentication/migrations/0017_auto_20230105_1743.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.14 on 2023-01-05 09:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0028_remove_app_tickets'), + ('authentication', '0016_auto_20221220_1956'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='from_ticket', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_token', to='tickets.applyloginassetticket', verbose_name='From ticket'), + ), + migrations.AddField( + model_name='connectiontoken', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Active'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index bb7d282b1..385294122 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -11,7 +11,7 @@ from rest_framework.exceptions import PermissionDenied from assets.const import Protocol from common.db.fields import EncryptCharField -from common.utils import lazyproperty, pretty_string, bulk_get +from common.utils import lazyproperty, pretty_string, bulk_get, reverse from common.utils.timezone import as_current_tz from orgs.mixins.models import JMSOrgBaseModel from terminal.models import Applet @@ -39,6 +39,12 @@ class ConnectionToken(JMSOrgBaseModel): user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_("Date expired")) + from_ticket = models.OneToOneField( + 'tickets.ApplyLoginAssetTicket', related_name='connection_token', + on_delete=models.SET_NULL, null=True, blank=True, + verbose_name=_('From ticket') + ) + is_active = models.BooleanField(default=True, verbose_name=_("Active")) class Meta: ordering = ('-date_expired',) @@ -90,6 +96,9 @@ class ConnectionToken(JMSOrgBaseModel): return self.permed_account.date_expired.timestamp() def is_valid(self): + if not self.is_active: + error = _('Connection token inactive') + raise PermissionDenied(error) if self.is_expired: error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) raise PermissionDenied(error) @@ -192,7 +201,7 @@ class ConnectionToken(JMSOrgBaseModel): @lazyproperty def account_object(self): - from assets.models import Account + from accounts.models import Account if not self.asset: return None diff --git a/apps/authentication/serializers/confirm.py b/apps/authentication/serializers/confirm.py index 9c0da463c..5d747c656 100644 --- a/apps/authentication/serializers/confirm.py +++ b/apps/authentication/serializers/confirm.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from ..const import ConfirmType, MFAType diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py index ac9345ae5..4585111ea 100644 --- a/apps/authentication/serializers/connect_token_secret.py +++ b/apps/authentication/serializers/connect_token_secret.py @@ -1,12 +1,13 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from accounts.const import SecretType +from accounts.models import Account from acls.models import CommandGroup, CommandFilterACL -from assets.const import SecretType -from assets.models import Asset, Account, Platform, Gateway, Domain +from assets.models import Asset, Platform, Gateway, Domain from assets.serializers import PlatformSerializer, AssetProtocolsSerializer -from common.drf.fields import LabeledChoiceField -from common.drf.fields import ObjectRelatedField +from common.serializers.fields import LabeledChoiceField +from common.serializers.fields import ObjectRelatedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField from users.models import User @@ -51,16 +52,15 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): class Meta: model = Account fields = [ - 'name', 'username', 'secret_type', 'secret', 'su_from', + 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged' ] class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer): """ Gateway """ - account = ObjectRelatedField( - required=False, source='select_account', queryset=Account.objects, - attrs=('id', 'name', 'username', 'secret', 'secret_type') + account = _SimpleAccountSerializer( + required=False, source='select_account', read_only=True ) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) @@ -135,6 +135,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): 'id', 'value', 'user', 'asset', 'account', 'platform', 'command_filter_acls', 'protocol', 'domain', 'gateway', 'actions', 'expire_at', + 'from_ticket', 'expire_now', 'connect_method', ] extra_kwargs = { diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 2fd9ae16e..c2805c78d 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from ..models import ConnectionToken @@ -13,8 +13,9 @@ __all__ = [ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) input_secret = EncryptedField( - label=_("Input secret"), max_length=40960, required=False, allow_blank=True + label=_("Input secret"), max_length=40960, required=False, allow_blank=True ) + from_ticket_info = serializers.SerializerMethodField(label=_("Ticket info")) class Meta: model = ConnectionToken @@ -22,6 +23,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): fields_small = fields_mini + [ 'user', 'asset', 'account', 'input_username', 'input_secret', 'connect_method', 'protocol', 'actions', + 'is_active', 'from_ticket', 'from_ticket_info', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', ] @@ -32,14 +34,25 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): ] fields = fields_small + read_only_fields extra_kwargs = { + 'from_ticket': {'read_only': True}, 'value': {'read_only': True}, } - def get_user(self, attrs): + def get_request_user(self): request = self.context.get('request') user = request.user if request else None return user + def get_user(self, attrs): + return self.get_request_user() + + def get_from_ticket_info(self, instance): + if not instance.from_ticket: + return {} + user = self.get_request_user() + info = instance.from_ticket.get_extra_info_of_review(user=user) + return info + class SuperConnectionTokenSerializer(ConnectionTokenSerializer): class Meta(ConnectionTokenSerializer.Meta): diff --git a/apps/authentication/serializers/password_mfa.py b/apps/authentication/serializers/password_mfa.py index 7684c3f88..d83260748 100644 --- a/apps/authentication/serializers/password_mfa.py +++ b/apps/authentication/serializers/password_mfa.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = [ 'MFAChallengeSerializer', 'MFASelectTypeSerializer', diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 340ece19c..e0e76f966 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -13,7 +13,7 @@ from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage -from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin +from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from common.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk from common.utils import FlashMessageUtil, get_logger @@ -27,7 +27,6 @@ from .mixins import METAMixin logger = get_logger(__file__) - DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' @@ -201,7 +200,7 @@ class DingTalkEnableStartView(UserVerifyPasswordView): class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') @@ -259,7 +258,7 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): class DingTalkOAuthLoginView(DingTalkOAuthMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') redirect_uri = reverse('authentication:dingtalk-oauth-login-callback', external=True) diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 4fdf6f846..16e111bad 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -13,7 +13,7 @@ from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage -from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin +from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from common.permissions import UserConfirmation from common.sdk.im.feishu import URL, FeiShu from common.utils import FlashMessageUtil, get_logger @@ -25,7 +25,6 @@ from users.views import UserVerifyPasswordView logger = get_logger(__file__) - FEISHU_STATE_SESSION_KEY = '_feishu_state' @@ -166,7 +165,7 @@ class FeiShuEnableStartView(UserVerifyPasswordView): class FeiShuQRLoginView(FeiShuQRMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index b896aa19e..c764c2138 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -15,7 +15,7 @@ from common.utils.random import random_string from common.utils.django import reverse, get_object_or_none from common.sdk.im.wecom import URL from common.sdk.im.wecom import WeCom -from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin +from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin from common.utils.common import get_request_ip from common.permissions import UserConfirmation from authentication import errors @@ -26,7 +26,6 @@ from .mixins import METAMixin logger = get_logger(__file__) - WECOM_STATE_SESSION_KEY = '_wecom_state' @@ -196,7 +195,7 @@ class WeComEnableStartView(UserVerifyPasswordView): class WeComQRLoginView(WeComQRMixin, METAMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) @@ -253,7 +252,7 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): class WeComOAuthLoginView(WeComOAuthMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') redirect_uri = reverse('authentication:wecom-oauth-login-callback', external=True) diff --git a/apps/common/mixins/api/__init__.py b/apps/common/api/__init__.py similarity index 75% rename from apps/common/mixins/api/__init__.py rename to apps/common/api/__init__.py index a5827ffef..7f2c4dbc9 100644 --- a/apps/common/mixins/api/__init__.py +++ b/apps/common/api/__init__.py @@ -1,7 +1,8 @@ -from .common import * from .action import * -from .patch import * +from .common import * from .filter import * +from .generic import * +from .mixin import * +from .patch import * from .permission import * -from .queryset import * from .serializer import * diff --git a/apps/common/mixins/api/action.py b/apps/common/api/action.py similarity index 100% rename from apps/common/mixins/api/action.py rename to apps/common/api/action.py diff --git a/apps/common/api.py b/apps/common/api/common.py similarity index 87% rename from apps/common/api.py rename to apps/common/api/common.py index 5bf20f027..8dfbc1ca9 100644 --- a/apps/common/api.py +++ b/apps/common/api/common.py @@ -9,16 +9,14 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import generics, serializers -from rest_framework.viewsets import GenericViewSet from common.permissions import IsValidUser -from .http import HttpResponseTemporaryRedirect -from .const import KEY_CACHE_RESOURCE_IDS -from .utils import get_logger -from .mixins import CommonApiMixin +from common.views.http import HttpResponseTemporaryRedirect +from common.utils import get_logger +from common.const import KEY_CACHE_RESOURCE_IDS __all__ = [ - 'LogTailApi', 'ResourcesIDCacheApi', 'CommonGenericViewSet' + 'LogTailApi', 'ResourcesIDCacheApi' ] logger = get_logger(__file__) @@ -102,10 +100,6 @@ class ResourcesIDCacheApi(APIView): def redirect_plural_name_api(request, *args, **kwargs): resource = kwargs.get("resource", "") org_full_path = request.get_full_path() - full_path = org_full_path.replace(resource, resource+"s", 1) + full_path = org_full_path.replace(resource, resource + "s", 1) logger.debug("Redirect {} => {}".format(org_full_path, full_path)) return HttpResponseTemporaryRedirect(full_path) - - -class CommonGenericViewSet(CommonApiMixin, GenericViewSet): - pass diff --git a/apps/common/mixins/api/filter.py b/apps/common/api/filter.py similarity index 100% rename from apps/common/mixins/api/filter.py rename to apps/common/api/filter.py diff --git a/apps/common/api/generic.py b/apps/common/api/generic.py new file mode 100644 index 000000000..6a02f125e --- /dev/null +++ b/apps/common/api/generic.py @@ -0,0 +1,21 @@ +from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework_bulk import BulkModelViewSet + +from .mixin import CommonApiMixin, RelationMixin +from .permission import AllowBulkDestroyMixin + + +class JMSGenericViewSet(CommonApiMixin, GenericViewSet): + pass + + +class JMSModelViewSet(CommonApiMixin, ModelViewSet): + pass + + +class JMSBulkModelViewSet(CommonApiMixin, AllowBulkDestroyMixin, BulkModelViewSet): + pass + + +class JMSBulkRelationModelViewSet(CommonApiMixin, RelationMixin, AllowBulkDestroyMixin, BulkModelViewSet): + pass diff --git a/apps/common/mixins/api/common.py b/apps/common/api/mixin.py similarity index 84% rename from apps/common/mixins/api/common.py rename to apps/common/api/mixin.py index 20e2eabc7..9517227a0 100644 --- a/apps/common/mixins/api/common.py +++ b/apps/common/api/mixin.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- # -from typing import Callable -from rest_framework.response import Response from collections import defaultdict +from typing import Callable from django.db.models.signals import m2m_changed +from rest_framework.response import Response -from .serializer import SerializerMixin -from .filter import ExtraFilterFieldsMixin from .action import RenderToJsonMixin -from .queryset import QuerySetMixin - +from .filter import ExtraFilterFieldsMixin +from .serializer import SerializerMixin __all__ = [ 'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin', @@ -82,11 +80,21 @@ class RelationMixin: self.send_m2m_changed_signal(instance, 'post_remove') +class QuerySetMixin: + action: str + get_serializer_class: Callable + get_queryset: Callable + + def get_queryset(self): + queryset = super().get_queryset() + if hasattr(self, 'action') and (self.action == 'list' or self.action == 'metadata'): + serializer_class = self.get_serializer_class() + if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): + queryset = serializer_class.setup_eager_loading(queryset) + return queryset + + class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, QuerySetMixin, RenderToJsonMixin, PaginatedResponseMixin): pass - - - - diff --git a/apps/common/mixins/api/patch.py b/apps/common/api/patch.py similarity index 100% rename from apps/common/mixins/api/patch.py rename to apps/common/api/patch.py diff --git a/apps/common/mixins/api/permission.py b/apps/common/api/permission.py similarity index 90% rename from apps/common/mixins/api/permission.py rename to apps/common/api/permission.py index 3a57658c3..19f63824f 100644 --- a/apps/common/mixins/api/permission.py +++ b/apps/common/api/permission.py @@ -5,7 +5,6 @@ from rest_framework.request import Request from common.utils import lazyproperty - __all__ = ['AllowBulkDestroyMixin', 'RoleAdminMixin', 'RoleUserMixin'] @@ -15,7 +14,8 @@ class AllowBulkDestroyMixin: 我们规定,批量删除的情况必须用 `id` 指定要删除的数据。 """ query = str(filtered.query) - return '`id` IN (' in query or '`id` =' in query + can = '`id` IN (' in query or '`id` =' in query or 'ptr_id` IN (' in query + return can class RoleAdminMixin: diff --git a/apps/common/mixins/api/serializer.py b/apps/common/api/serializer.py similarity index 100% rename from apps/common/mixins/api/serializer.py rename to apps/common/api/serializer.py diff --git a/apps/common/compat.py b/apps/common/compat.py deleted file mode 100644 index f2e757625..000000000 --- a/apps/common/compat.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -""" -兼容Python版本 -""" - -import sys - -is_py2 = (sys.version_info[0] == 2) -is_py3 = (sys.version_info[0] == 3) - - -try: - import simplejson as json -except (ImportError, SyntaxError): - import json - - -if is_py2: - - def to_bytes(data): - """若输入为unicode, 则转为utf-8编码的bytes;其他则原样返回。""" - if isinstance(data, unicode): - return data.encode('utf-8') - else: - return data - - def to_string(data): - """把输入转换为str对象""" - return to_bytes(data) - - def to_unicode(data): - """把输入转换为unicode,要求输入是unicode或者utf-8编码的bytes。""" - if isinstance(data, bytes): - return data.decode('utf-8') - else: - return data - - def stringify(input): - if isinstance(input, dict): - return dict([(stringify(key), stringify(value)) for key,value in input.iteritems()]) - elif isinstance(input, list): - return [stringify(element) for element in input] - elif isinstance(input, unicode): - return input.encode('utf-8') - else: - return input - - builtin_str = str - bytes = str - str = unicode - - -elif is_py3: - - def to_bytes(data): - """若输入为str(即unicode),则转为utf-8编码的bytes;其他则原样返回""" - if isinstance(data, str): - return data.encode(encoding='utf-8') - else: - return data - - def to_string(data): - """若输入为bytes,则认为是utf-8编码,并返回str""" - if isinstance(data, bytes): - return data.decode('utf-8') - else: - return data - - def to_unicode(data): - """把输入转换为unicode,要求输入是unicode或者utf-8编码的bytes。""" - return to_string(data) - - def stringify(input): - return input - - builtin_str = str - bytes = bytes - str = str - diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index d4a58e618..d66eb229c 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from common.local import add_encrypted_field_set from common.utils import signer, crypto +from .validators import PortRangeValidator __all__ = [ "JsonMixin", @@ -27,7 +28,9 @@ __all__ = [ "EncryptJsonDictTextField", "EncryptJsonDictCharField", "PortField", + "PortRangeField", "BitChoices", + "TreeChoices", ] @@ -216,15 +219,15 @@ class PortField(models.IntegerField): super().__init__(*args, **kwargs) -class BitChoices(models.IntegerChoices): +class TreeChoices(models.Choices): + @classmethod + def is_tree(cls): + return True + @classmethod def branches(cls): return [i for i in cls] - @classmethod - def is_tree(cls): - return False - @classmethod def tree(cls): if not cls.is_tree(): @@ -234,7 +237,7 @@ class BitChoices(models.IntegerChoices): @classmethod def render_node(cls, node): - if isinstance(node, BitChoices): + if isinstance(node, models.Choices): return { "value": node.name, "label": node.label, @@ -247,9 +250,26 @@ class BitChoices(models.IntegerChoices): "children": [cls.render_node(child) for child in children], } + @classmethod + def all(cls): + return [i[0] for i in cls.choices] + + +class BitChoices(models.IntegerChoices, TreeChoices): + @classmethod + def is_tree(cls): + return False + @classmethod def all(cls): value = 0 for c in cls: value |= c.value return value + + +class PortRangeField(models.CharField): + def __init__(self, **kwargs): + kwargs['max_length'] = 16 + super().__init__(**kwargs) + self.validators.append(PortRangeValidator()) diff --git a/apps/common/db/managers.py b/apps/common/db/managers.py new file mode 100644 index 000000000..053178071 --- /dev/null +++ b/apps/common/db/managers.py @@ -0,0 +1,13 @@ +from django.db import models + + +class DebugQueryManager(models.Manager): + def get_queryset(self): + import traceback + lines = traceback.format_stack() + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + for line in lines[-10:-1]: + print(line) + print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + queryset = super().get_queryset() + return queryset diff --git a/apps/common/mixins/models.py b/apps/common/db/mixins.py similarity index 76% rename from apps/common/mixins/models.py rename to apps/common/db/mixins.py index f6705f53c..43c5f8a65 100644 --- a/apps/common/mixins/models.py +++ b/apps/common/db/mixins.py @@ -41,15 +41,3 @@ class NoDeleteModelMixin(models.Model): self.is_discard = True self.discard_time = timezone.now() return self.save() - - -class DebugQueryManager(models.Manager): - def get_queryset(self): - import traceback - lines = traceback.format_stack() - print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") - for line in lines[-10:-1]: - print(line) - print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") - queryset = super().get_queryset() - return queryset diff --git a/apps/common/db/models.py b/apps/common/db/models.py index ae726b85d..4d292827a 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -21,47 +21,6 @@ from django.utils.translation import ugettext_lazy as _ from ..const.signals import SKIP_SIGNAL -class BitOperationChoice: - NONE = 0 - NAME_MAP: dict - DB_CHOICES: tuple - NAME_MAP_REVERSE: dict - - @classmethod - def value_to_choices(cls, value): - if isinstance(value, list): - return value - value = int(value) - choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] - return choices - - @classmethod - def value_to_choices_display(cls, value): - choices = cls.value_to_choices(value) - return [str(dict(cls.choices())[i]) for i in choices] - - @classmethod - def choices_to_value(cls, value): - if not isinstance(value, list): - return cls.NONE - db_value = [ - cls.NAME_MAP_REVERSE[v] for v in value - if v in cls.NAME_MAP_REVERSE.keys() - ] - if not db_value: - return cls.NONE - - def to_choices(x, y): - return x | y - - result = reduce(to_choices, db_value) - return result - - @classmethod - def choices(cls): - return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] - - class ChoicesMixin: _value2label_map_: dict diff --git a/apps/common/db/validators.py b/apps/common/db/validators.py new file mode 100644 index 000000000..9f8db0278 --- /dev/null +++ b/apps/common/db/validators.py @@ -0,0 +1,22 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + + +class PortRangeValidator: + def __init__(self, start=1, end=65535): + self.start = start + self.end = end + self.error_message = _("Invalid port range, should be like and within {}-{}").format(start, end) + + def __call__(self, data): + try: + _range = data.split('-') + if len(_range) != 2: + raise ValueError('') + _range = [int(i) for i in _range] + if _range[0] > _range[1]: + raise ValueError('') + if _range[0] < self.start or _range[1] > self.end: + raise ValueError('') + except ValueError: + raise ValidationError(self.error_message) diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py deleted file mode 100644 index 23567aa32..000000000 --- a/apps/common/drf/api.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet, ViewSet -from rest_framework_bulk import BulkModelViewSet - -from ..mixins.api import ( - RelationMixin, AllowBulkDestroyMixin, CommonApiMixin -) - - -class JMSGenericViewSet(CommonApiMixin, GenericViewSet): - pass - - -class JMSViewSet(CommonApiMixin, ViewSet): - pass - - -class JMSModelViewSet(CommonApiMixin, ModelViewSet): - pass - - -class JMSReadOnlyModelViewSet(CommonApiMixin, ReadOnlyModelViewSet): - pass - - -class JMSBulkModelViewSet(CommonApiMixin, AllowBulkDestroyMixin, BulkModelViewSet): - pass - - -class JMSBulkRelationModelViewSet(CommonApiMixin, - RelationMixin, - AllowBulkDestroyMixin, - BulkModelViewSet): - pass diff --git a/apps/common/drf/exc_handlers.py b/apps/common/drf/exc_handlers.py index b99a53547..f832042a7 100644 --- a/apps/common/drf/exc_handlers.py +++ b/apps/common/drf/exc_handlers.py @@ -1,13 +1,14 @@ +from logging import getLogger + from django.core.exceptions import PermissionDenied, ObjectDoesNotExist as DJObjectDoesNotExist +from django.db.models.deletion import ProtectedError from django.http import Http404 from django.utils.translation import gettext -from django.db.models.deletion import ProtectedError from rest_framework import exceptions -from rest_framework.views import set_rollback from rest_framework.response import Response +from rest_framework.views import set_rollback from common.exceptions import JMSObjectDoesNotExist, ReferencedByOthers -from logging import getLogger logger = getLogger('drf_exception') unexpected_exception_logger = getLogger('unexpected_exception') @@ -27,7 +28,7 @@ def extract_object_name(exc, index=0): def common_exception_handler(exc, context): - logger.exception('') + # logger.exception('') if isinstance(exc, Http404): exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 1)) diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index a4b5bf7ad..731daf878 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -14,7 +14,7 @@ from rest_framework.fields import empty from rest_framework.metadata import SimpleMetadata from rest_framework.request import clone_request -from common.drf.fields import TreeChoicesMixin +from common.serializers.fields import TreeChoicesField class SimpleMetadataWithFilters(SimpleMetadata): @@ -22,14 +22,9 @@ class SimpleMetadataWithFilters(SimpleMetadata): methods = {"PUT", "POST", "GET", "PATCH"} attrs = [ - "read_only", - "label", - "help_text", - "min_length", - "max_length", - "min_value", - "max_value", - "write_only", + "read_only", "label", "help_text", + "min_length", "max_length", "min_value", + "max_value", "write_only", ] def determine_actions(self, request, view): @@ -71,6 +66,8 @@ class SimpleMetadataWithFilters(SimpleMetadata): class_name = field.__class__.__name__ if class_name == "LabeledChoiceField": tp = "labeled_choice" + elif class_name == "JSONField": + tp = 'json' elif class_name == "ObjectRelatedField": tp = "object_related_field" elif class_name == "ManyRelatedField": @@ -119,7 +116,7 @@ class SimpleMetadataWithFilters(SimpleMetadata): elif getattr(field, "fields", None): field_info["children"] = self.get_serializer_info(field) - if isinstance(field, TreeChoicesMixin): + if isinstance(field, TreeChoicesField): self.set_tree_field(field, field_info) elif isinstance(field, serializers.ChoiceField): self.set_choices_field(field, field_info) diff --git a/apps/common/management/commands/services/services/celery_base.py b/apps/common/management/commands/services/services/celery_base.py index 92fa99cba..7d69139b2 100644 --- a/apps/common/management/commands/services/services/celery_base.py +++ b/apps/common/management/commands/services/services/celery_base.py @@ -1,5 +1,5 @@ -from ..hands import * from .base import BaseService +from ..hands import * class CeleryBaseService(BaseService): @@ -12,9 +12,12 @@ class CeleryBaseService(BaseService): @property def cmd(self): print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize())) - + ansible_config_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'ansible.cfg') + ansible_modules_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'modules') os.environ.setdefault('PYTHONOPTIMIZE', '1') os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True') + os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path) + os.environ.setdefault('ANSIBLE_LIBRARY', ansible_modules_path) if os.getuid() == 0: os.environ.setdefault('C_FORCE_ROOT', '1') diff --git a/apps/common/mixins/__init__.py b/apps/common/mixins/__init__.py deleted file mode 100644 index b2a7ec7e4..000000000 --- a/apps/common/mixins/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# -from .models import * -from .api import * -from .views import * diff --git a/apps/common/mixins/api/queryset.py b/apps/common/mixins/api/queryset.py deleted file mode 100644 index 4f56e8a51..000000000 --- a/apps/common/mixins/api/queryset.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# - -__all__ = ['QuerySetMixin'] - - -class QuerySetMixin: - def get_queryset(self): - queryset = super().get_queryset() - serializer_class = self.get_serializer_class() - - if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): - queryset = serializer_class.setup_eager_loading(queryset) - return queryset diff --git a/apps/common/drf/serializers/__init__.py b/apps/common/serializers/__init__.py similarity index 100% rename from apps/common/drf/serializers/__init__.py rename to apps/common/serializers/__init__.py diff --git a/apps/common/drf/serializers/common.py b/apps/common/serializers/common.py similarity index 100% rename from apps/common/drf/serializers/common.py rename to apps/common/serializers/common.py diff --git a/apps/common/drf/fields.py b/apps/common/serializers/fields.py similarity index 77% rename from apps/common/drf/fields.py rename to apps/common/serializers/fields.py index 7c9f55eee..96ff7c103 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/serializers/fields.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- # -import six from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.fields import ChoiceField, empty -from common.db.fields import BitChoices -from common.utils import decrypt_password +from common.db.fields import TreeChoices from common.local import add_encrypted_field_set +from common.utils import decrypt_password __all__ = [ "ReadableHiddenField", @@ -16,7 +15,8 @@ __all__ = [ "LabeledChoiceField", "ObjectRelatedField", "BitChoicesField", - "TreeChoicesMixin" + "TreeChoicesField", + "LabeledMultipleChoiceField", ] @@ -55,16 +55,14 @@ class LabeledChoiceField(ChoiceField): def __init__(self, *args, **kwargs): super(LabeledChoiceField, self).__init__(*args, **kwargs) self.choice_mapper = { - six.text_type(key): value for key, value in self.choices.items() + key: value for key, value in self.choices.items() } - def to_representation(self, value): - if value is None: - return value - return { - "value": value, - "label": self.choice_mapper.get(six.text_type(value), value), - } + def to_representation(self, key): + if key is None: + return key + label = self.choice_mapper.get(key) + return {"value": key, "label": label} def to_internal_value(self, data): if isinstance(data, dict): @@ -72,6 +70,31 @@ class LabeledChoiceField(ChoiceField): return super(LabeledChoiceField, self).to_internal_value(data) +class LabeledMultipleChoiceField(serializers.MultipleChoiceField): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.choice_mapper = { + key: value for key, value in self.choices.items() + } + + def to_representation(self, keys): + if keys is None: + return keys + return [ + {"value": key, "label": self.choice_mapper.get(key)} + for key in keys + ] + + def to_internal_value(self, data): + if not data: + return data + + if isinstance(data[0], dict): + return [item.get("value") for item in data] + else: + return data + + class ObjectRelatedField(serializers.RelatedField): default_error_messages = { "required": _("This field is required."), @@ -87,6 +110,8 @@ class ObjectRelatedField(serializers.RelatedField): def to_representation(self, value): data = {} for attr in self.attrs: + if not hasattr(value, attr): + continue data[attr] = getattr(value, attr) return data @@ -106,22 +131,28 @@ class ObjectRelatedField(serializers.RelatedField): self.fail("incorrect_type", data_type=type(pk).__name__) -class TreeChoicesMixin: - tree = [] - - -class BitChoicesField(TreeChoicesMixin, serializers.MultipleChoiceField): - """ - 位字段 - """ - +class TreeChoicesField(serializers.MultipleChoiceField): def __init__(self, choice_cls, **kwargs): - assert issubclass(choice_cls, BitChoices) + assert issubclass(choice_cls, TreeChoices) choices = [(c.name, c.label) for c in choice_cls] self.tree = choice_cls.tree() self._choice_cls = choice_cls super().__init__(choices=choices, **kwargs) + def to_internal_value(self, data): + if not data: + return data + if isinstance(data[0], dict): + return [item.get("value") for item in data] + else: + return data + + +class BitChoicesField(TreeChoicesField): + """ + 位字段 + """ + def to_representation(self, value): if isinstance(value, list) and len(value) == 1: # Swagger 会使用 field.choices.keys() 迭代传递进来 diff --git a/apps/common/drf/serializers/mixin.py b/apps/common/serializers/mixin.py similarity index 96% rename from apps/common/drf/serializers/mixin.py rename to apps/common/serializers/mixin.py index acda64505..749a39820 100644 --- a/apps/common/drf/serializers/mixin.py +++ b/apps/common/serializers/mixin.py @@ -8,13 +8,15 @@ from rest_framework.fields import SkipField, empty from rest_framework.settings import api_settings from rest_framework.utils import html -from common.drf.fields import EncryptedField -from ..fields import LabeledChoiceField, ObjectRelatedField +from common.serializers.fields import EncryptedField +from common.serializers.fields import LabeledChoiceField, ObjectRelatedField __all__ = [ 'BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin', - 'SecretReadableMixin', + 'SecretReadableMixin', 'CommonModelSerializer', + 'CommonBulkModelSerializer', + ] @@ -355,5 +357,13 @@ class CommonSerializerMixin(DynamicFieldsMixin, RelatedModelSerializerMixin, pass +class CommonModelSerializer(CommonSerializerMixin, serializers.ModelSerializer): + pass + + class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): pass + + +class CommonBulkModelSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): + pass diff --git a/apps/common/signal_handlers.py b/apps/common/signal_handlers.py index e14a9fdf8..4471edb94 100644 --- a/apps/common/signal_handlers.py +++ b/apps/common/signal_handlers.py @@ -34,6 +34,8 @@ class Counter: def on_request_finished_logging_db_query(sender, **kwargs): queries = connection.queries counters = defaultdict(Counter) + table_queries = defaultdict(list) + for query in queries: if not query['sql'] or not query['sql'].startswith('SELECT'): continue @@ -44,22 +46,38 @@ def on_request_finished_logging_db_query(sender, **kwargs): counters[table_name].time += float(time) counters['total'].counter += 1 counters['total'].time += float(time) + table_queries[table_name].append(query) counters = sorted(counters.items(), key=lambda x: x[1]) if not counters: return + method = 'GET' path = '/Unknown' current_request = get_current_request() if current_request: method = current_request.method path = current_request.get_full_path() + logger.debug(">>> [{}] {}".format(method, path)) for name, counter in counters: logger.debug("Query {:3} times using {:.2f}s {}".format( counter.counter, counter.time, name) ) + # print(">>> [{}] {}".format(method, path)) + # for table_name, queries in table_queries.items(): + # if table_name.startswith('rbac_') or table_name.startswith('auth_permission'): + # continue + # print("- Table: {}".format(table_name)) + # for i, query in enumerate(queries, 1): + # sql = query['sql'] + # if not sql or not sql.startswith('SELECT'): + # continue + # print('\t{}. {}'.format(i, sql)) + + on_request_finished_release_local(sender, **kwargs) + def on_request_finished_release_local(sender, **kwargs): thread_local.__release_local__() diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py index ab39c4fa2..b684f004a 100644 --- a/apps/common/utils/http.py +++ b/apps/common/utils/http.py @@ -5,6 +5,8 @@ import threading import time from email.utils import formatdate +from rest_framework.serializers import BooleanField + _STRPTIME_LOCK = threading.Lock() _GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" @@ -37,4 +39,9 @@ def iso8601_to_unixtime(time_string): return to_unixtime(time_string, _ISO8601_FORMAT) +def get_remote_addr(request): + return request.META.get("HTTP_X_FORWARDED_HOST") or request.META.get("REMOTE_ADDR") + +def is_true(value): + return value in BooleanField.TRUE_VALUES diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py index abf4d6056..85589de69 100644 --- a/apps/common/utils/verify_code.py +++ b/apps/common/utils/verify_code.py @@ -21,12 +21,12 @@ def send_async(sender): class SendAndVerifyCodeUtil(object): KEY_TMPL = 'auth-verify-code-{}' - def __init__(self, target, code=None, key=None, backend='email', timeout=60, **kwargs): - self.target = target + def __init__(self, target, code=None, key=None, backend='email', timeout=None, **kwargs): self.code = code - self.timeout = timeout + self.target = target self.backend = backend self.key = key or self.KEY_TMPL.format(target) + self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout self.other_args = kwargs def gen_and_send_async(self): diff --git a/apps/common/views/__init__.py b/apps/common/views/__init__.py new file mode 100644 index 000000000..8f5452f1e --- /dev/null +++ b/apps/common/views/__init__.py @@ -0,0 +1 @@ +from .msg import * diff --git a/apps/common/http.py b/apps/common/views/http.py similarity index 56% rename from apps/common/http.py rename to apps/common/views/http.py index 5a3d85524..b1bff5754 100644 --- a/apps/common/http.py +++ b/apps/common/views/http.py @@ -3,8 +3,6 @@ from django.http import HttpResponse from django.utils.encoding import iri_to_uri -from rest_framework.serializers import BooleanField - class HttpResponseTemporaryRedirect(HttpResponse): status_code = 307 @@ -13,10 +11,3 @@ class HttpResponseTemporaryRedirect(HttpResponse): HttpResponse.__init__(self) self['Location'] = iri_to_uri(redirect_to) - -def get_remote_addr(request): - return request.META.get("HTTP_X_FORWARDED_HOST") or request.META.get("REMOTE_ADDR") - - -def is_true(value): - return value in BooleanField.TRUE_VALUES diff --git a/apps/common/mixins/views.py b/apps/common/views/mixins.py similarity index 98% rename from apps/common/mixins/views.py rename to apps/common/views/mixins.py index d1feb61d8..39b146b3f 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/views/mixins.py @@ -8,7 +8,6 @@ from rest_framework.request import Request from common.exceptions import UserConfirmRequired from audits.handler import create_or_update_operate_log -from audits.models import OperateLog from audits.const import ActionChoices __all__ = [ diff --git a/apps/common/views.py b/apps/common/views/msg.py similarity index 100% rename from apps/common/views.py rename to apps/common/views/msg.py diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 8c5f39642..a3e0dc7ac 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -343,7 +343,7 @@ class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView): http_method_names = ['get'] def check_permissions(self, request): - return request.user.has_perm(['rbac.view_audit', 'rbac.view_console']) + return request.user.has_perm('rbac.view_audit | rbac.view_console') def get(self, request, *args, **kwargs): data = {} diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d43384987..55b94c87f 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -464,6 +464,7 @@ class Config(dict): 'SECURITY_LUNA_REMEMBER_AUTH': True, 'SECURITY_WATERMARK_ENABLED': True, 'SECURITY_MFA_VERIFY_TTL': 3600, + 'VERIFY_CODE_TTL': 60, 'SECURITY_SESSION_SHARE': True, 'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True, 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5, @@ -504,8 +505,8 @@ class Config(dict): 'GMSSL_ENABLED': False, # 操作日志变更字段的存储ES配置 'OPERATE_LOG_ELASTICSEARCH_CONFIG': {}, - # Magnus 组件需要监听的端口范围 - 'MAGNUS_PORTS': '30000-30100', + # Magnus 组件需要监听的 Oracle 端口范围 + 'MAGNUS_ORACLE_PORTS': '30000-30030', # 记录清理清理 'LOGIN_LOG_KEEP_DAYS': 200, @@ -834,11 +835,13 @@ class ConfigManager: sys.path.insert(0, PROJECT_DIR) try: from config import config as c + except ImportError: + return False + if c: self.from_object(c) return True - except ImportError: - pass - return False + else: + return False def load_from_yml(self): for i in ['config.yml', 'config.yaml']: diff --git a/apps/jumpserver/middleware.py b/apps/jumpserver/middleware.py index bf7aa7945..f76077b7f 100644 --- a/apps/jumpserver/middleware.py +++ b/apps/jumpserver/middleware.py @@ -1,17 +1,21 @@ # ~*~ coding: utf-8 ~*~ +import json import os import re -import pytz import time -import json -from django.utils import timezone -from django.shortcuts import HttpResponse +import pytz +from channels.db import database_sync_to_async from django.conf import settings from django.core.exceptions import MiddlewareNotUsed +from django.core.handlers.asgi import ASGIRequest from django.http.response import HttpResponseForbidden +from django.shortcuts import HttpResponse +from django.utils import timezone +from authentication.backends.drf import (SignatureAuthentication, + AccessTokenAuthentication) from .utils import set_current_request @@ -129,3 +133,35 @@ class EndMiddleware: response = self.get_response(request) request._e_time_end = time.time() return response + + +@database_sync_to_async +def get_signature_user(scope): + headers = dict(scope["headers"]) + if not headers.get(b'authorization'): + return + if scope['type'] == 'websocket': + scope['method'] = 'GET' + try: + # 因为 ws 使用的是 scope,所以需要转换成 request 对象,用于认证校验 + request = ASGIRequest(scope, None) + backends = [SignatureAuthentication(), + AccessTokenAuthentication()] + for backend in backends: + user, _ = backend.authenticate(request) + if user: + return user + except Exception as e: + print(e) + return None + + +class WsSignatureAuthMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + user = await get_signature_user(scope) + if user: + scope['user'] = user + return await self.app(scope, receive, send) diff --git a/apps/jumpserver/routing.py b/apps/jumpserver/routing.py index 773baee99..965a2b71b 100644 --- a/apps/jumpserver/routing.py +++ b/apps/jumpserver/routing.py @@ -5,13 +5,17 @@ from django.core.asgi import get_asgi_application from ops.urls.ws_urls import urlpatterns as ops_urlpatterns from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns from settings.urls.ws_urls import urlpatterns as setting_urlpatterns +from terminal.urls.ws_urls import urlpatterns as terminal_urlpatterns + +from .middleware import WsSignatureAuthMiddleware urlpatterns = [] -urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns +urlpatterns += ops_urlpatterns + \ + notifications_urlpatterns + \ + setting_urlpatterns + \ + terminal_urlpatterns application = ProtocolTypeRouter({ - 'websocket': AuthMiddlewareStack( - URLRouter(urlpatterns) - ), + 'websocket': WsSignatureAuthMiddleware(AuthMiddlewareStack(URLRouter(urlpatterns))), "http": get_asgi_application(), }) diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index d1fb7ba9a..d74e604d3 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -80,6 +80,7 @@ INSTALLED_APPS = [ 'orgs.apps.OrgsConfig', 'users.apps.UsersConfig', 'assets.apps.AssetsConfig', + 'accounts.apps.AccountsConfig', 'perms.apps.PermsConfig', 'ops.apps.OpsConfig', 'settings.apps.SettingsConfig', @@ -349,8 +350,10 @@ if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory' else: REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % { - 'protocol': REDIS_PROTOCOL, 'password': CONFIG.REDIS_PASSWORD, - 'host': CONFIG.REDIS_HOST, 'port': CONFIG.REDIS_PORT, + 'protocol': REDIS_PROTOCOL, + 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, } REDIS_CACHE_DEFAULT = { diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 37bb073c6..7cb92915d 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -50,6 +50,7 @@ SECURITY_PASSWORD_RULES = [ 'SECURITY_PASSWORD_NUMBER', 'SECURITY_PASSWORD_SPECIAL_CHAR' ] +VERIFY_CODE_TTL = CONFIG.VERIFY_CODE_TTL SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION @@ -187,4 +188,4 @@ OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE # Magnus DB Port -MAGNUS_PORTS = CONFIG.MAGNUS_PORTS +MAGNUS_ORACLE_PORTS = CONFIG.MAGNUS_ORACLE_PORTS diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 92e72e843..0c35ba322 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -31,7 +31,6 @@ REST_FRAMEWORK = { ), 'DEFAULT_AUTHENTICATION_CLASSES': ( # 'rest_framework.authentication.BasicAuthentication', - 'authentication.backends.drf.AccessKeyAuthentication', 'authentication.backends.drf.AccessTokenAuthentication', 'authentication.backends.drf.PrivateTokenAuthentication', 'authentication.backends.drf.SignatureAuthentication', @@ -119,7 +118,6 @@ else: REDIS_LAYERS_SSL_PARAMS.pop('ssl', None) REDIS_LAYERS_HOST['address'] = '{}?{}'.format(REDIS_LAYERS_ADDRESS, urlencode(REDIS_LAYERS_SSL_PARAMS)) - CHANNEL_LAYERS = { 'default': { 'BACKEND': 'common.cache.RedisChannelLayer', diff --git a/apps/jumpserver/settings/logging.py b/apps/jumpserver/settings/logging.py index ec2b3d9c2..a021d1716 100644 --- a/apps/jumpserver/settings/logging.py +++ b/apps/jumpserver/settings/logging.py @@ -50,7 +50,7 @@ LOGGING = { 'encoding': 'utf8', 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', - 'maxBytes': 1024*1024*100, + 'maxBytes': 1024 * 1024 * 100, 'backupCount': 7, 'formatter': 'main', 'filename': JUMPSERVER_LOG_FILE, @@ -60,7 +60,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'main', - 'maxBytes': 1024*1024*100, + 'maxBytes': 1024 * 1024 * 100, 'backupCount': 7, 'filename': ANSIBLE_LOG_FILE, }, @@ -136,12 +136,6 @@ LOGGING = { } } -if CONFIG.DEBUG_DEV: - LOGGING['loggers']['django.db'] = { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' - } - SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 730796b1d..2218edd6c 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -13,6 +13,7 @@ api_v1 = [ path('index/', api.IndexApi.as_view()), path('users/', include('users.urls.api_urls', namespace='api-users')), path('assets/', include('assets.urls.api_urls', namespace='api-assets')), + path('accounts/', include('accounts.urls', namespace='api-accounts')), path('perms/', include('perms.urls.api_urls', namespace='api-perms')), path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), path('ops/', include('ops.urls.api_urls', namespace='api-ops')), @@ -43,7 +44,6 @@ if settings.XPACK_ENABLED: path('xpack/', include('xpack.urls.api_urls', namespace='api-xpack')) ) - urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('api/v1/', include(api_v1)), @@ -58,7 +58,7 @@ urlpatterns = [ # 静态文件处理路由 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ - + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # js i18n 路由文件 urlpatterns += [ @@ -78,6 +78,5 @@ if os.environ.get('DEBUG_TOOLBAR', False): path('__debug__/', include('debug_toolbar.urls')), ] - handler404 = 'jumpserver.views.handler404' handler500 = 'jumpserver.views.handler500' diff --git a/apps/jumpserver/views/index.py b/apps/jumpserver/views/index.py index 639f6d683..c443d9c40 100644 --- a/apps/jumpserver/views/index.py +++ b/apps/jumpserver/views/index.py @@ -1,7 +1,7 @@ from django.views.generic import View from django.shortcuts import redirect from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin __all__ = ['IndexView'] diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py index 9cf5a5500..984898158 100644 --- a/apps/jumpserver/views/other.py +++ b/apps/jumpserver/views/other.py @@ -11,8 +11,7 @@ from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse from rest_framework.views import APIView -from common.http import HttpResponseTemporaryRedirect - +from common.views.http import HttpResponseTemporaryRedirect __all__ = [ 'LunaView', 'I18NView', 'KokoView', 'WsView', @@ -23,8 +22,9 @@ __all__ = [ class LunaView(View): def get(self, request): - msg = _("
Luna is a separately deployed program, you need to deploy Luna, koko, configure nginx for url distribution,
" - "If you see this page, prove that you are not accessing the nginx listening port. Good luck.") + msg = _( + "
Luna is a separately deployed program, you need to deploy Luna, koko, configure nginx for url distribution,
" + "If you see this page, prove that you are not accessing the nginx listening port. Good luck.") return HttpResponse(msg) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 3072a7280..c4d0c146f 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3969f0fedc2f554a846f5e5f48070db1f954e22cb2aea50dab766bc74fd8eafc -size 119587 +oid sha256:eb850ffd130e7cad2ea8c186f94a059c6a882dd1526f7a4c4a16d2fea2a1815b +size 119290 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index ff458f85e..063cce72e 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-27 12:11+0800\n" +"POT-Creation-Date: 2023-01-16 14:24+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,684 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" +#: accounts/api/automations/base.py:79 +msgid "The parameter 'action' must be [{}]" +msgstr "パラメータ 'action' は [{}] でなければなりません。" + +#: accounts/const/account.py:6 +#: accounts/serializers/automations/change_secret.py:33 +#: assets/models/_user.py:35 audits/signal_handlers.py:51 +#: authentication/confirm/password.py:9 authentication/forms.py:32 +#: authentication/templates/authentication/login.html:288 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 +#: users/forms/profile.py:22 users/serializers/user.py:97 +#: users/templates/users/_msg_user_created.html:13 +#: users/templates/users/user_password_verify.html:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 +msgid "Password" +msgstr "パスワード" + +#: accounts/const/account.py:7 +#: accounts/serializers/automations/change_secret.py:34 +#, fuzzy +msgid "SSH key" +msgstr "SSHキー" + +#: accounts/const/account.py:8 authentication/models/access_key.py:33 +msgid "Access key" +msgstr "アクセスキー" + +#: accounts/const/account.py:9 assets/models/_user.py:38 +#: authentication/models/sso_token.py:14 +msgid "Token" +msgstr "トークン" + +#: accounts/const/account.py:13 common/db/fields.py:235 +#: settings/serializers/terminal.py:14 +msgid "All" +msgstr "すべて" + +#: accounts/const/account.py:14 +msgid "Manual input" +msgstr "手動入力" + +#: accounts/const/account.py:15 +msgid "Dynamic user" +msgstr "動的コード" + +#: accounts/const/account.py:19 users/models/user.py:631 +msgid "Local" +msgstr "ローカル" + +#: accounts/const/account.py:20 +msgid "Collected" +msgstr "" + +#: accounts/const/automation.py:22 rbac/tree.py:51 +#, fuzzy +msgid "Push account" +msgstr "サービスアカウントです" + +#: accounts/const/automation.py:23 +#, fuzzy +msgid "Change secret" +msgstr "秘密を改める" + +#: accounts/const/automation.py:24 +#, fuzzy +msgid "Verify account" +msgstr "パスワード/キーの確認" + +#: accounts/const/automation.py:25 +#, fuzzy +msgid "Gather accounts" +msgstr "アカウントを集める" + +#: accounts/const/automation.py:43 +#, fuzzy +#| msgid "Set password" +msgid "Specific password" +msgstr "パスワードの設定" + +#: accounts/const/automation.py:44 +msgid "Random" +msgstr "" + +#: accounts/const/automation.py:48 ops/const.py:13 +msgid "Append SSH KEY" +msgstr "追加" + +#: accounts/const/automation.py:49 ops/const.py:14 +msgid "Empty and append SSH KEY" +msgstr "すべてクリアして追加" + +#: accounts/const/automation.py:50 ops/const.py:15 +msgid "Replace (The key generated by JumpServer) " +msgstr "置換(JumpServerによって生成された鍵)" + +#: accounts/const/automation.py:55 +#, fuzzy +#| msgid "Date created" +msgid "On asset create" +msgstr "作成された日付" + +#: accounts/const/automation.py:58 +#, fuzzy +#| msgid "After change" +msgid "On perm add user" +msgstr "変更後" + +#: accounts/const/automation.py:60 +msgid "On perm add user group" +msgstr "" + +#: accounts/const/automation.py:62 +#, fuzzy +#| msgid "permed assets" +msgid "On perm add asset" +msgstr "パーマ資産" + +#: accounts/const/automation.py:64 +#, fuzzy +#| msgid "After change" +msgid "On perm add node" +msgstr "変更後" + +#: accounts/const/automation.py:66 +#, fuzzy +msgid "On perm add account" +msgstr "アカウントを集める" + +#: accounts/const/automation.py:68 +#, fuzzy +#| msgid "Add asset to node" +msgid "On asset join node" +msgstr "ノードにアセットを追加する" + +#: accounts/const/automation.py:70 +#, fuzzy +#| msgid "User group" +msgid "On user join group" +msgstr "ユーザーグループ" + +#: accounts/const/automation.py:78 +#, fuzzy +#| msgid "After change" +msgid "On perm change" +msgstr "変更後" + +#: accounts/const/automation.py:85 +#, fuzzy +#| msgid "Perm ungroup node" +msgid "Inherit from group or node" +msgstr "グループ化されていないノードを表示" + +#: accounts/const/automation.py:93 +#, fuzzy +#| msgid "Created by" +msgid "Create and push" +msgstr "によって作成された" + +#: accounts/const/automation.py:94 +#, fuzzy +#| msgid "Date created" +msgid "Only create" +msgstr "作成された日付" + +#: accounts/models/account.py:47 accounts/serializers/account/account.py:77 +#: accounts/serializers/automations/change_secret.py:107 +#: accounts/serializers/automations/change_secret.py:127 acls/models/base.py:96 +#: acls/serializers/base.py:56 assets/models/asset/common.py:96 +#: assets/models/asset/common.py:281 assets/models/cmd_filter.py:36 +#: assets/serializers/domain.py:19 assets/serializers/label.py:27 +#: audits/models.py:34 authentication/models/connection_token.py:32 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:27 +#: terminal/backends/command/models.py:21 +#: terminal/backends/command/serializers.py:14 +#: terminal/models/session/session.py:31 terminal/notifications.py:93 +#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:220 +msgid "Asset" +msgstr "資産" + +#: accounts/models/account.py:51 accounts/serializers/account/account.py:81 +#: authentication/serializers/connect_token_secret.py:49 +msgid "Su from" +msgstr "から切り替え" + +#: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 +#: terminal/models/applet/applet.py:25 +msgid "Version" +msgstr "バージョン" + +#: accounts/models/account.py:55 accounts/serializers/account/account.py:78 +#: users/models/user.py:727 +msgid "Source" +msgstr "ソース" + +#: accounts/models/account.py:58 +#: accounts/serializers/automations/change_secret.py:108 +#: accounts/serializers/automations/change_secret.py:128 acls/models/base.py:98 +#: acls/serializers/base.py:57 assets/serializers/asset/common.py:125 +#: assets/serializers/gateway.py:30 audits/models.py:35 ops/models/base.py:18 +#: terminal/backends/command/models.py:22 terminal/models/session/session.py:33 +#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 +msgid "Account" +msgstr "アカウント" + +#: accounts/models/account.py:64 +msgid "Can view asset account secret" +msgstr "資産アカウントの秘密を表示できます" + +#: accounts/models/account.py:65 +msgid "Can change asset account secret" +msgstr "資産口座の秘密を変更できます" + +#: accounts/models/account.py:66 +msgid "Can view asset history account" +msgstr "資産履歴アカウントを表示できます" + +#: accounts/models/account.py:67 +msgid "Can view asset history account secret" +msgstr "資産履歴アカウントパスワードを表示できます" + +#: accounts/models/account.py:104 accounts/serializers/account/account.py:16 +#, fuzzy +msgid "Account template" +msgstr "アカウント名" + +#: accounts/models/account.py:109 +#, fuzzy +msgid "Can view asset account template secret" +msgstr "資産アカウントの秘密を表示できます" + +#: accounts/models/account.py:110 +#, fuzzy +msgid "Can change asset account template secret" +msgstr "資産口座の秘密を変更できます" + +#: accounts/models/automations/backup_account.py:25 +#: accounts/models/automations/change_secret.py:47 +#: accounts/serializers/account/backup.py:29 +#: accounts/serializers/automations/change_secret.py:56 +msgid "Recipient" +msgstr "受信者" + +#: accounts/models/automations/backup_account.py:34 +#: accounts/models/automations/backup_account.py:96 +msgid "Account backup plan" +msgstr "アカウントバックアップ計画" + +#: accounts/models/automations/backup_account.py:77 +#: assets/models/automations/base.py:102 audits/models.py:41 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:107 +#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:108 +#: terminal/models/session/session.py:43 +#: tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:19 +msgid "Date start" +msgstr "開始日" + +#: accounts/models/automations/backup_account.py:80 +#: authentication/templates/authentication/_msg_oauth_bind.html:11 +#: notifications/notifications.py:186 +msgid "Time" +msgstr "時間" + +#: accounts/models/automations/backup_account.py:84 +msgid "Account backup snapshot" +msgstr "アカウントのバックアップスナップショット" + +#: accounts/models/automations/backup_account.py:88 +#: accounts/serializers/automations/base.py:42 +#: assets/models/automations/base.py:109 +#: assets/serializers/automations/base.py:40 +msgid "Trigger mode" +msgstr "トリガーモード" + +#: accounts/models/automations/backup_account.py:91 audits/models.py:130 +#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "理由" + +#: accounts/models/automations/backup_account.py:93 +#: accounts/serializers/automations/change_secret.py:106 +#: accounts/serializers/automations/change_secret.py:129 +#: ops/serializers/job.py:54 terminal/serializers/session.py:49 +msgid "Is success" +msgstr "成功は" + +#: accounts/models/automations/backup_account.py:101 +msgid "Account backup execution" +msgstr "アカウントバックアップの実行" + +#: accounts/models/automations/base.py:15 +#, fuzzy +msgid "Account automation task" +msgstr "自動管理" + +#: accounts/models/automations/base.py:25 +#, fuzzy +msgid "Automation execution" +msgstr "インスタンスタスクの同期実行" + +#: accounts/models/automations/base.py:26 +#, fuzzy +msgid "Automation executions" +msgstr "インスタンスタスクの同期実行" + +#: accounts/models/automations/base.py:28 +msgid "Can view change secret execution" +msgstr "改密実行の表示" + +#: accounts/models/automations/base.py:29 +msgid "Can add change secret execution" +msgstr "改密実行の作成" + +#: accounts/models/automations/base.py:31 +msgid "Can view gather accounts execution" +msgstr "収集アカウント実行の表示" + +#: accounts/models/automations/base.py:32 +msgid "Can add gather accounts execution" +msgstr "収集アカウントの作成実行" + +#: accounts/models/automations/base.py:34 +#, fuzzy +#| msgid "Can view gather accounts execution" +msgid "Can view push account execution" +msgstr "収集アカウント実行の表示" + +#: accounts/models/automations/base.py:35 +#, fuzzy +#| msgid "Can add gather accounts execution" +msgid "Can add push account execution" +msgstr "収集アカウントの作成実行" + +#: accounts/models/automations/change_secret.py:17 accounts/models/base.py:36 +#: accounts/serializers/account/account.py:114 +#: accounts/serializers/account/base.py:16 +#: accounts/serializers/automations/change_secret.py:46 +#: authentication/serializers/connect_token_secret.py:40 +#: authentication/serializers/connect_token_secret.py:50 +msgid "Secret type" +msgstr "鍵の種類" + +#: accounts/models/automations/change_secret.py:21 +#: accounts/serializers/automations/change_secret.py:40 +msgid "Secret strategy" +msgstr "鍵ポリシー" + +#: accounts/models/automations/change_secret.py:23 +#: accounts/models/automations/change_secret.py:72 accounts/models/base.py:38 +#: accounts/serializers/account/base.py:19 +#: authentication/models/temp_token.py:10 +#: authentication/templates/authentication/_access_key_modal.html:31 +#: settings/serializers/auth/radius.py:19 +msgid "Secret" +msgstr "ひみつ" + +#: accounts/models/automations/change_secret.py:24 +msgid "Password rules" +msgstr "パスワードルール" + +#: accounts/models/automations/change_secret.py:27 +#, fuzzy +msgid "SSH key change strategy" +msgstr "SSHキー戦略" + +#: accounts/models/automations/change_secret.py:54 +#, fuzzy +msgid "Change secret automation" +msgstr "セキュリティ設定を変更できます" + +#: accounts/models/automations/change_secret.py:71 +#, fuzzy +msgid "Old secret" +msgstr "OTP 秘密" + +#: accounts/models/automations/change_secret.py:73 +#, fuzzy +msgid "Date started" +msgstr "開始日" + +#: accounts/models/automations/change_secret.py:74 +#: assets/models/automations/base.py:103 ops/models/base.py:56 +#: ops/models/celery.py:64 ops/models/job.py:108 +#: terminal/models/applet/host.py:109 +msgid "Date finished" +msgstr "終了日" + +#: accounts/models/automations/change_secret.py:76 common/const/choices.py:20 +#, fuzzy +msgid "Error" +msgstr "企業微信エラー" + +#: accounts/models/automations/change_secret.py:80 +#, fuzzy +msgid "Change secret record" +msgstr "パスワードの変更" + +#: accounts/models/automations/gather_account.py:15 +#: accounts/tasks/gather_accounts.py:28 +#, fuzzy +msgid "Gather asset accounts" +msgstr "アカウントを集める" + +#: accounts/models/automations/push_account.py:13 +#, fuzzy +#| msgid "Trigger mode" +msgid "Triggers" +msgstr "トリガーモード" + +#: accounts/models/automations/push_account.py:14 accounts/models/base.py:34 +#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: assets/models/_user.py:34 audits/models.py:115 authentication/forms.py:25 +#: authentication/forms.py:27 authentication/models/temp_token.py:9 +#: authentication/templates/authentication/_msg_different_city.html:9 +#: authentication/templates/authentication/_msg_oauth_bind.html:9 +#: users/forms/profile.py:32 users/forms/profile.py:112 +#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 +msgid "Username" +msgstr "ユーザー名" + +#: accounts/models/automations/push_account.py:15 acls/models/base.py:77 +#: acls/serializers/base.py:81 assets/models/cmd_filter.py:81 +#: audits/models.py:51 audits/serializers.py:75 +#: authentication/serializers/connect_token_secret.py:108 +#: authentication/templates/authentication/_access_key_modal.html:34 +msgid "Action" +msgstr "アクション" + +#: accounts/models/automations/push_account.py:41 +#, fuzzy +msgid "Push asset account" +msgstr "サービスアカウントです" + +#: accounts/models/automations/verify_account.py:15 +#, fuzzy +msgid "Verify asset account" +msgstr "パスワード/キーの確認" + +#: accounts/models/base.py:33 acls/models/base.py:71 +#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: applications/models.py:9 assets/models/_user.py:33 +#: assets/models/asset/common.py:94 assets/models/asset/common.py:106 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 +#: assets/models/group.py:20 assets/models/label.py:18 +#: assets/models/platform.py:20 assets/models/platform.py:74 +#: assets/serializers/asset/common.py:143 assets/serializers/platform.py:128 +#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 +#: ops/models/adhoc.py:22 ops/models/celery.py:15 ops/models/celery.py:57 +#: ops/models/job.py:25 ops/models/playbook.py:14 orgs/models.py:69 +#: perms/models/asset_permission.py:56 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 +#: terminal/models/applet/applet.py:23 terminal/models/component/endpoint.py:12 +#: terminal/models/component/endpoint.py:90 +#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 +#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: users/models/group.py:13 users/models/user.py:675 +#: xpack/plugins/cloud/models.py:28 +msgid "Name" +msgstr "名前" + +#: accounts/models/base.py:39 +msgid "Privileged" +msgstr "" + +#: accounts/models/base.py:40 assets/models/asset/common.py:113 +#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 +#: assets/models/label.py:22 +#: authentication/serializers/connect_token_secret.py:106 +#: terminal/models/applet/applet.py:28 users/serializers/user.py:158 +msgid "Is active" +msgstr "アクティブです。" + +#: accounts/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "アカウントバックアップルートタスクの結果の通知" + +#: accounts/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "" +"{} -アカウントバックアップの通過タスクが完了しました。詳細は添付ファイルをご" +"覧ください" + +#: accounts/notifications.py:20 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} -アカウントのバックアップ通過タスクが完了しました: 暗号化パスワードが設定" +"されていません-個人情報にアクセスしてください-> ファイル暗号化パスワードを設" +"定してください暗号化パスワード" + +#: accounts/notifications.py:31 +msgid "Notification of implementation result of encryption change plan" +msgstr "暗号化変更プランの実装結果の通知" + +#: accounts/notifications.py:41 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください" + +#: accounts/notifications.py:42 +msgid "" +"{} - The encryption change task has been completed: the encryption password " +"has not been set - please go to personal information -> file encryption " +"password to set the encryption password" +msgstr "" +"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" +"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" + +#: accounts/serializers/account/account.py:19 +#: assets/serializers/asset/common.py:52 +msgid "Push now" +msgstr "" + +#: accounts/serializers/account/account.py:21 +#: accounts/serializers/account/base.py:64 +#, fuzzy +msgid "Has secret" +msgstr "ひみつ" + +#: accounts/serializers/account/account.py:28 +#: assets/serializers/asset/common.py:79 +msgid "Account template not found" +msgstr "" + +#: accounts/serializers/account/account.py:73 +#, fuzzy +#| msgid "Asset Info" +msgid "Asset not found" +msgstr "資産情報" + +#: accounts/serializers/account/backup.py:27 +#: accounts/serializers/automations/base.py:35 +#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 +#: settings/serializers/auth/ldap.py:66 +msgid "Periodic perform" +msgstr "定期的なパフォーマンス" + +#: accounts/serializers/account/backup.py:28 +#: accounts/serializers/automations/gather_accounts.py:23 +#, fuzzy +msgid "Executed amount" +msgstr "実行時間" + +#: accounts/serializers/account/backup.py:30 +#: accounts/serializers/automations/change_secret.py:57 +msgid "Currently only mail sending is supported" +msgstr "現在、メール送信のみがサポートされています" + +#: accounts/serializers/account/base.py:24 +msgid "Key password" +msgstr "キーパスワード" + +#: accounts/serializers/account/base.py:80 +msgid "Specific" +msgstr "" + +#: accounts/serializers/automations/base.py:21 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:35 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "資産" + +#: accounts/serializers/automations/base.py:22 +#: assets/models/asset/common.py:112 assets/models/automations/base.py:18 +#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:67 +msgid "Nodes" +msgstr "ノード" + +#: accounts/serializers/automations/base.py:40 +#: assets/models/automations/base.py:105 +#: assets/serializers/automations/base.py:39 +#, fuzzy +msgid "Automation snapshot" +msgstr "製造オーダスナップショット" + +#: accounts/serializers/automations/base.py:41 acls/models/command_acl.py:24 +#: acls/serializers/command_acl.py:18 applications/models.py:14 +#: assets/models/_user.py:46 assets/models/automations/base.py:20 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:76 +#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:86 +#: audits/serializers.py:47 +#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:33 +#: perms/serializers/user_permission.py:26 terminal/models/applet/applet.py:27 +#: terminal/models/component/storage.py:57 +#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:28 +#: terminal/serializers/session.py:26 terminal/serializers/storage.py:181 +#: tickets/models/comment.py:26 tickets/models/flow.py:56 +#: tickets/models/ticket/apply_application.py:16 +#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53 +#: tickets/serializers/ticket/ticket.py:19 +msgid "Type" +msgstr "タイプ" + +#: accounts/serializers/automations/change_secret.py:43 +msgid "SSH Key strategy" +msgstr "SSHキー戦略" + +#: accounts/serializers/automations/change_secret.py:76 +msgid "* Please enter the correct password length" +msgstr "* 正しいパスワードの長さを入力してください" + +#: accounts/serializers/automations/change_secret.py:80 +msgid "* Password length range 6-30 bits" +msgstr "* パスワードの長さの範囲6-30ビット" + +#: accounts/serializers/automations/change_secret.py:110 +#: assets/models/automations/base.py:114 +#, fuzzy +msgid "Automation task execution" +msgstr "インスタンスタスクの同期実行" + +#: accounts/serializers/automations/change_secret.py:150 audits/const.py:45 +#: audits/models.py:40 common/const/choices.py:18 ops/const.py:51 +#: ops/serializers/celery.py:39 terminal/const.py:59 +#: terminal/models/session/sharing.py:103 tickets/views/approve.py:114 +msgid "Success" +msgstr "成功" + +#: accounts/serializers/automations/change_secret.py:151 +#: assets/const/automation.py:8 audits/const.py:46 common/const/choices.py:19 +#: ops/const.py:53 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失敗しました" + +#: accounts/tasks/automation.py:11 +#, fuzzy +msgid "Account execute automation" +msgstr "バッチ実行コマンド" + +#: accounts/tasks/backup_account.py:13 +#, fuzzy +msgid "Execute account backup plan" +msgstr "アカウントバックアップ計画" + +#: accounts/tasks/gather_accounts.py:31 +#, fuzzy +msgid "Gather assets accounts" +msgstr "資産ユーザーの収集" + +#: accounts/tasks/push_account.py:30 accounts/tasks/push_account.py:36 +#, fuzzy +msgid "Push accounts to assets" +msgstr "システムユーザーを資産にプッシュする:" + +#: accounts/tasks/verify_account.py:30 +msgid "Verify asset account availability" +msgstr "" + +#: accounts/tasks/verify_account.py:36 +#, fuzzy +msgid "Verify accounts connectivity" +msgstr "テストアカウント接続:" + +#: accounts/utils.py:42 +msgid "Password can not contains `{{` " +msgstr "パスワードには '{{' を含まない" + +#: accounts/utils.py:45 +msgid "Password can not contains `'` " +msgstr "パスワードには `'` を含まない" + +#: accounts/utils.py:47 +msgid "Password can not contains `\"` " +msgstr "パスワードには `\"` を含まない" + +#: accounts/utils.py:53 +msgid "private key invalid or passphrase error" +msgstr "秘密鍵が無効またはpassphraseエラー" + #: acls/apps.py:7 msgid "Acls" msgstr "Acls" @@ -35,67 +713,39 @@ msgstr "受け入れられる" msgid "Review" msgstr "レビュー担当者" -#: acls/models/base.py:71 acls/models/command_acl.py:21 -#: acls/serializers/base.py:34 applications/models.py:9 -#: assets/models/_user.py:33 assets/models/asset/common.py:92 -#: assets/models/asset/common.py:101 assets/models/base.py:64 -#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:20 assets/models/platform.py:71 -#: assets/serializers/asset/common.py:87 assets/serializers/platform.py:121 -#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 -#: ops/models/adhoc.py:23 ops/models/celery.py:15 ops/models/job.py:24 -#: ops/models/playbook.py:14 orgs/models.py:67 -#: perms/models/asset_permission.py:55 rbac/models/role.py:29 -#: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/applet/applet.py:22 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:86 -#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 -#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/user.py:675 -#: xpack/plugins/cloud/models.py:28 -msgid "Name" -msgstr "名前" - #: acls/models/base.py:73 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:89 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:93 msgid "Priority" msgstr "優先順位" #: acls/models/base.py:74 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:90 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:94 msgid "1-100, the lower the value will be match first" msgstr "1-100、低い値は最初に一致します" -#: acls/models/base.py:77 acls/serializers/base.py:63 -#: assets/models/cmd_filter.py:81 audits/models.py:51 audits/serializers.py:73 -#: authentication/serializers/connect_token_secret.py:108 -#: authentication/templates/authentication/_access_key_modal.html:34 -msgid "Action" -msgstr "アクション" - -#: acls/models/base.py:78 acls/serializers/base.py:59 +#: acls/models/base.py:78 acls/serializers/base.py:75 #: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:81 msgid "Reviewers" msgstr "レビュー担当者" #: acls/models/base.py:79 authentication/models/access_key.py:17 +#: authentication/models/connection_token.py:47 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:75 terminal/models/session/sharing.py:27 +#: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 msgid "Active" msgstr "アクティブ" -#: acls/models/base.py:91 acls/models/login_acl.py:13 +#: acls/models/base.py:94 acls/models/login_acl.py:13 #: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:30 -#: audits/models.py:49 audits/models.py:93 +#: audits/models.py:49 audits/models.py:99 #: authentication/models/connection_token.py:28 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 -#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:57 -#: perms/serializers/permission.py:23 rbac/builtin.py:120 +#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 +#: perms/serializers/permission.py:23 rbac/builtin.py:118 #: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 #: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 @@ -105,35 +755,8 @@ msgstr "アクティブ" msgid "User" msgstr "ユーザー" -#: acls/models/base.py:93 acls/serializers/base.py:56 -#: assets/models/account.py:50 assets/models/asset/common.py:94 -#: assets/models/asset/common.py:221 assets/models/cmd_filter.py:36 -#: assets/models/gathered_user.py:12 assets/serializers/account/account.py:76 -#: assets/serializers/automations/change_secret.py:100 -#: assets/serializers/automations/change_secret.py:122 -#: assets/serializers/domain.py:19 assets/serializers/gathered_user.py:11 -#: assets/serializers/label.py:27 audits/models.py:34 -#: authentication/models/connection_token.py:32 -#: perms/models/asset_permission.py:63 perms/serializers/permission.py:27 -#: terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 -#: terminal/models/session/session.py:31 terminal/notifications.py:93 -#: xpack/plugins/cloud/models.py:220 -msgid "Asset" -msgstr "資産" - -#: acls/models/base.py:95 acls/serializers/base.py:57 -#: assets/models/account.py:60 -#: assets/serializers/automations/change_secret.py:101 -#: assets/serializers/automations/change_secret.py:123 audits/models.py:35 -#: ops/models/base.py:18 terminal/backends/command/models.py:22 -#: terminal/models/session/session.py:33 -#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 -msgid "Account" -msgstr "アカウント" - #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: terminal/backends/command/serializers.py:15 +#: ops/serializers/job.py:53 terminal/backends/command/serializers.py:15 #: terminal/models/session/session.py:41 terminal/serializers/session.py:19 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -144,23 +767,6 @@ msgstr "コマンド" msgid "Regex" msgstr "正規情報" -#: acls/models/command_acl.py:24 acls/serializers/command_acl.py:19 -#: applications/models.py:14 assets/models/_user.py:46 -#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 -#: assets/models/platform.py:73 assets/serializers/asset/common.py:63 -#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:86 -#: audits/serializers.py:45 -#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:32 -#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:26 -#: terminal/models/component/storage.py:57 -#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:33 -#: terminal/serializers/session.py:25 tickets/models/comment.py:26 -#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 -#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:54 -#: tickets/serializers/ticket/ticket.py:19 -msgid "Type" -msgstr "タイプ" - #: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 #: settings/serializers/basic.py:10 xpack/plugins/license/models.py:29 msgid "Content" @@ -174,7 +780,8 @@ msgstr "1行1コマンド" msgid "Ignore case" msgstr "家を無視する" -#: acls/models/command_acl.py:33 acls/serializers/command_acl.py:29 +#: acls/models/command_acl.py:33 acls/models/command_acl.py:96 +#: acls/serializers/command_acl.py:28 #: authentication/serializers/connect_token_secret.py:78 msgid "Command group" msgstr "コマンドグループ" @@ -183,10 +790,6 @@ msgstr "コマンドグループ" msgid "The generated regular expression is incorrect: {}" msgstr "生成された正規表現が正しくありません: {}" -#: acls/models/command_acl.py:96 -msgid "Commands" -msgstr "コマンド#コマンド#" - #: acls/models/command_acl.py:100 msgid "Command acl" msgstr "コマンドフィルタリング" @@ -195,7 +798,7 @@ msgstr "コマンドフィルタリング" msgid "Command confirm" msgstr "コマンドの確認" -#: acls/models/login_acl.py:16 +#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:29 msgid "Rule" msgstr "ルール" @@ -219,19 +822,6 @@ msgstr "ログイン資産の確認" msgid "Format for comma-delimited string, with * indicating a match all. " msgstr "コンマ区切り文字列の形式。* はすべて一致することを示します。" -#: acls/serializers/base.py:18 acls/serializers/base.py:49 -#: assets/models/_user.py:34 assets/models/base.py:65 -#: assets/models/gathered_user.py:13 audits/models.py:109 -#: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models/temp_token.py:9 -#: authentication/templates/authentication/_msg_different_city.html:9 -#: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: users/forms/profile.py:32 users/forms/profile.py:112 -#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 -msgid "Username" -msgstr "ユーザー名" - #: acls/serializers/base.py:25 msgid "" "Format for comma-delimited string, with * indicating a match all. Such as: " @@ -246,14 +836,50 @@ msgstr "" msgid "IP/Host" msgstr "IP/ホスト" -#: acls/serializers/base.py:90 tickets/serializers/ticket/ticket.py:78 +#: acls/serializers/base.py:60 +#, fuzzy +#| msgid "System user name" +msgid "User (username)" +msgstr "システムユーザー名" + +#: acls/serializers/base.py:64 +#, fuzzy +#| msgid "Asset hostname" +msgid "Asset (name)" +msgstr "資産ホスト名" + +#: acls/serializers/base.py:68 +#, fuzzy +#| msgid "Address" +msgid "Asset (address)" +msgstr "アドレス" + +#: acls/serializers/base.py:72 +#, fuzzy +#| msgid "Account name" +msgid "Account (username)" +msgstr "アカウント名" + +#: acls/serializers/base.py:78 acls/serializers/login_acl.py:27 +#, fuzzy +#| msgid "Reviewers" +msgid "Reviewers amount" +msgstr "レビュー担当者" + +#: acls/serializers/base.py:109 tickets/serializers/ticket/ticket.py:76 msgid "The organization `{}` does not exist" msgstr "組織 '{}'は存在しません" -#: acls/serializers/base.py:96 +#: acls/serializers/base.py:115 msgid "None of the reviewers belong to Organization `{}`" msgstr "いずれのレビューアも組織 '{}' に属していません" +#: acls/serializers/command_acl.py:31 +#, fuzzy +#| msgid "Command amount" +msgid "Command group amount" +msgstr "コマンド量" + #: acls/serializers/rules/rules.py:20 #: xpack/plugins/cloud/serializers/task.py:22 msgid "IP address invalid: `{}`" @@ -269,11 +895,11 @@ msgstr "" "192.168.10.1、192.168.1.0/24、10.1.1.1-10.1.1.20、2001:db8:2de::e13、2001:" "db8:1a:1110::/64" -#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:102 +#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:107 #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61 msgid "IP" msgstr "IP" @@ -286,9 +912,9 @@ msgid "Applications" msgstr "アプリケーション" #: applications/models.py:11 assets/models/label.py:21 -#: assets/models/platform.py:72 assets/serializers/asset/common.py:62 +#: assets/models/platform.py:75 assets/serializers/asset/common.py:121 #: assets/serializers/cagegory.py:8 assets/serializers/platform.py:87 -#: assets/serializers/platform.py:122 perms/serializers/user_permission.py:23 +#: assets/serializers/platform.py:129 perms/serializers/user_permission.py:25 #: settings/models.py:35 tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "カテゴリ" @@ -307,7 +933,7 @@ msgid "Can match application" msgstr "アプリケーションを一致させることができます" #: applications/serializers/attrs/application_type/clickhouse.py:11 -#: assets/models/asset/common.py:93 assets/models/platform.py:21 +#: assets/models/asset/common.py:95 assets/models/platform.py:21 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" @@ -321,11 +947,7 @@ msgstr "" "デフォルトポートは9000で、HTTPインタフェースとネイティブインタフェースは異な" "るポートを使用する" -#: assets/api/automations/base.py:77 -msgid "The parameter 'action' must be [{}]" -msgstr "パラメータ 'action' は [{}] でなければなりません。" - -#: assets/api/domain.py:56 +#: assets/api/domain.py:57 msgid "Number required" msgstr "必要な数" @@ -345,110 +967,59 @@ msgstr "削除に失敗し、ノードにアセットが含まれています。 msgid "App assets" msgstr "アプリ資産" -#: assets/automations/base/manager.py:123 +#: assets/automations/base/manager.py:76 #, fuzzy msgid "{} disabled" msgstr "無効" -#: assets/const/account.py:6 audits/const.py:6 audits/const.py:64 +#: assets/automations/ping_gateway/manager.py:33 +#: authentication/models/connection_token.py:113 +#, fuzzy +msgid "No account" +msgstr "アカウント" + +#: assets/automations/ping_gateway/manager.py:55 +#, fuzzy, python-brace-format +msgid "Unable to connect to port {port} on {address}" +msgstr "{ip} でポート {port} に接続できません" + +#: assets/automations/ping_gateway/manager.py:58 +#: authentication/middleware.py:76 xpack/plugins/cloud/providers/fc.py:48 +msgid "Authentication failed" +msgstr "認証に失敗しました" + +#: assets/automations/ping_gateway/manager.py:60 +#: assets/automations/ping_gateway/manager.py:86 +msgid "Connect failed" +msgstr "接続に失敗しました" + +#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:35 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" msgstr "不明" -#: assets/const/account.py:7 +#: assets/const/automation.py:7 msgid "Ok" msgstr "OK" -#: assets/const/account.py:8 -#: assets/serializers/automations/change_secret.py:118 -#: assets/serializers/automations/change_secret.py:146 audits/const.py:75 -#: common/const/choices.py:19 ops/const.py:51 xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失敗しました" - -#: assets/const/account.py:12 assets/models/_user.py:35 -#: audits/signal_handlers.py:49 authentication/confirm/password.py:9 -#: authentication/forms.py:32 -#: authentication/templates/authentication/login.html:288 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:97 -#: users/templates/users/_msg_user_created.html:13 -#: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/cloud/serializers/account_attrs.py:28 -msgid "Password" -msgstr "パスワード" - -#: assets/const/account.py:13 -#, fuzzy -msgid "SSH key" -msgstr "SSHキー" - -#: assets/const/account.py:14 authentication/models/access_key.py:33 -msgid "Access key" -msgstr "アクセスキー" - -#: assets/const/account.py:15 assets/models/_user.py:38 -#: authentication/models/sso_token.py:14 -msgid "Token" -msgstr "トークン" - -#: assets/const/automation.py:13 +#: assets/const/automation.py:12 msgid "Ping" msgstr "" +#: assets/const/automation.py:13 +#, fuzzy +#| msgid "Test gateway" +msgid "Ping gateway" +msgstr "テストゲートウェイ" + #: assets/const/automation.py:14 #, fuzzy msgid "Gather facts" msgstr "アカウントを集める" -#: assets/const/automation.py:15 -#, fuzzy -msgid "Create account" -msgstr "アカウントを集める" - -#: assets/const/automation.py:16 -#, fuzzy -msgid "Change secret" -msgstr "秘密を改める" - -#: assets/const/automation.py:17 -#, fuzzy -msgid "Verify account" -msgstr "パスワード/キーの確認" - -#: assets/const/automation.py:18 -#, fuzzy -msgid "Gather accounts" -msgstr "アカウントを集める" - -#: assets/const/automation.py:38 assets/serializers/account/base.py:29 -msgid "Specific" -msgstr "" - -#: assets/const/automation.py:39 ops/const.py:20 -msgid "All assets use the same random password" -msgstr "すべての資産は同じランダムパスワードを使用します" - -#: assets/const/automation.py:40 ops/const.py:21 -msgid "All assets use different random password" -msgstr "すべての資産は異なるランダムパスワードを使用します" - -#: assets/const/automation.py:44 ops/const.py:13 -msgid "Append SSH KEY" -msgstr "追加" - -#: assets/const/automation.py:45 ops/const.py:14 -msgid "Empty and append SSH KEY" -msgstr "すべてクリアして追加" - -#: assets/const/automation.py:46 ops/const.py:15 -msgid "Replace (The key generated by JumpServer) " -msgstr "置換(JumpServerによって生成された鍵)" - #: assets/const/category.py:11 settings/serializers/auth/radius.py:16 -#: settings/serializers/auth/sms.py:67 terminal/models/applet/applet.py:129 -#: terminal/models/component/endpoint.py:13 +#: settings/serializers/auth/sms.py:67 terminal/models/component/endpoint.py:13 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "ホスト" @@ -457,8 +1028,8 @@ msgstr "ホスト" msgid "Device" msgstr "" -#: assets/const/category.py:13 assets/models/asset/database.py:8 -#: assets/models/asset/database.py:34 +#: assets/const/category.py:13 assets/models/asset/database.py:9 +#: assets/models/asset/database.py:32 msgid "Database" msgstr "データベース" @@ -467,12 +1038,12 @@ msgstr "データベース" msgid "Cloud service" msgstr "クラウドセンター" -#: assets/const/category.py:15 audits/const.py:62 -#: terminal/models/applet/applet.py:20 +#: assets/const/category.py:15 audits/const.py:33 +#: terminal/models/applet/applet.py:21 msgid "Web" msgstr "" -#: assets/const/device.py:7 terminal/models/applet/applet.py:19 +#: assets/const/device.py:7 terminal/models/applet/applet.py:20 #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -490,7 +1061,7 @@ msgstr "" msgid "Firewall" msgstr "" -#: assets/const/types.py:181 +#: assets/const/types.py:200 #, fuzzy #| msgid "MFA type" msgid "All types" @@ -527,33 +1098,33 @@ msgstr "SSHパブリックキー" #: assets/models/_user.py:40 assets/models/cmd_filter.py:40 #: assets/models/cmd_filter.py:88 assets/models/group.py:23 -#: assets/models/platform.py:76 common/db/models.py:78 ops/models/adhoc.py:29 -#: ops/models/job.py:40 ops/models/playbook.py:17 rbac/models/role.py:37 -#: settings/models.py:38 terminal/models/applet/applet.py:31 -#: terminal/models/applet/applet.py:131 terminal/models/applet/host.py:110 -#: terminal/models/component/endpoint.py:20 -#: terminal/models/component/endpoint.py:96 +#: assets/models/platform.py:79 common/db/models.py:37 ops/models/adhoc.py:28 +#: ops/models/job.py:41 ops/models/playbook.py:17 rbac/models/role.py:37 +#: settings/models.py:38 terminal/models/applet/applet.py:32 +#: terminal/models/applet/applet.py:137 terminal/models/applet/host.py:110 +#: terminal/models/component/endpoint.py:24 +#: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:45 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:297 users/models/user.py:714 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:119 msgid "Comment" msgstr "コメント" -#: assets/models/_user.py:41 assets/models/automations/base.py:91 +#: assets/models/_user.py:41 assets/models/automations/base.py:101 #: assets/models/cmd_filter.py:41 assets/models/group.py:22 -#: common/db/models.py:76 ops/models/base.py:54 ops/models/job.py:105 +#: common/db/models.py:35 ops/models/base.py:54 ops/models/job.py:106 #: users/models/user.py:932 msgid "Date created" msgstr "作成された日付" #: assets/models/_user.py:42 assets/models/cmd_filter.py:42 -#: common/db/models.py:77 +#: common/db/models.py:36 msgid "Date updated" msgstr "更新日" #: assets/models/_user.py:43 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:21 -#: common/db/models.py:74 users/models/user.py:722 +#: common/db/models.py:33 users/models/user.py:722 #: users/serializers/group.py:33 msgid "Created by" msgstr "によって作成された" @@ -564,8 +1135,8 @@ msgstr "ユーザーと同じユーザー名" #: assets/models/_user.py:48 authentication/models/connection_token.py:37 #: authentication/serializers/connect_token_secret.py:103 -#: terminal/models/applet/applet.py:29 terminal/serializers/session.py:24 -#: terminal/serializers/session.py:40 terminal/serializers/storage.py:68 +#: terminal/models/applet/applet.py:30 terminal/serializers/session.py:24 +#: terminal/serializers/session.py:45 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "プロトコル" @@ -577,7 +1148,7 @@ msgstr "オートプッシュ" msgid "Sudo" msgstr "すど" -#: assets/models/_user.py:51 ops/const.py:44 ops/models/adhoc.py:19 +#: assets/models/_user.py:51 ops/const.py:44 msgid "Shell" msgstr "シェル" @@ -613,143 +1184,78 @@ msgstr "システムユーザー" msgid "Can match system user" msgstr "システムユーザーに一致できます" -#: assets/models/account.py:44 common/db/fields.py:232 -#: settings/serializers/terminal.py:14 -msgid "All" -msgstr "すべて" - -#: assets/models/account.py:45 -msgid "Manual input" -msgstr "手動入力" - -#: assets/models/account.py:46 -msgid "Dynamic user" -msgstr "動的コード" - -#: assets/models/account.py:54 assets/serializers/account/account.py:79 -#: authentication/serializers/connect_token_secret.py:48 -msgid "Su from" -msgstr "から切り替え" - -#: assets/models/account.py:56 settings/serializers/auth/cas.py:20 -#: terminal/models/applet/applet.py:24 -msgid "Version" -msgstr "バージョン" - -#: assets/models/account.py:66 -msgid "Can view asset account secret" -msgstr "資産アカウントの秘密を表示できます" - -#: assets/models/account.py:67 -msgid "Can change asset account secret" -msgstr "資産口座の秘密を変更できます" - -#: assets/models/account.py:68 -msgid "Can view asset history account" -msgstr "資産履歴アカウントを表示できます" - -#: assets/models/account.py:69 -msgid "Can view asset history account secret" -msgstr "資産履歴アカウントパスワードを表示できます" - -#: assets/models/account.py:106 assets/serializers/account/account.py:15 -#, fuzzy -msgid "Account template" -msgstr "アカウント名" - -#: assets/models/account.py:111 -#, fuzzy -msgid "Can view asset account template secret" -msgstr "資産アカウントの秘密を表示できます" - -#: assets/models/account.py:112 -#, fuzzy -msgid "Can change asset account template secret" -msgstr "資産口座の秘密を変更できます" - -#: assets/models/asset/common.py:103 assets/models/platform.py:109 -#: assets/serializers/asset/common.py:65 +#: assets/models/asset/common.py:108 assets/models/platform.py:112 #: authentication/serializers/connect_token_secret.py:107 -#: perms/serializers/user_permission.py:21 +#: perms/serializers/user_permission.py:23 #: xpack/plugins/cloud/serializers/account_attrs.py:179 msgid "Platform" msgstr "プラットフォーム" -#: assets/models/asset/common.py:105 assets/models/domain.py:21 -#: assets/serializers/asset/common.py:64 +#: assets/models/asset/common.py:110 assets/models/domain.py:21 #: authentication/serializers/connect_token_secret.py:125 +#: perms/serializers/user_permission.py:27 msgid "Domain" msgstr "ドメイン" -#: assets/models/asset/common.py:107 assets/models/automations/base.py:18 -#: assets/models/cmd_filter.py:32 assets/serializers/asset/common.py:66 -#: assets/serializers/automations/base.py:21 -#: perms/models/asset_permission.py:66 -msgid "Nodes" -msgstr "ノード" - -#: assets/models/asset/common.py:108 assets/models/automations/base.py:21 -#: assets/models/base.py:71 assets/models/cmd_filter.py:39 -#: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:106 -#: terminal/models/applet/applet.py:27 users/serializers/user.py:158 -msgid "Is active" -msgstr "アクティブです。" - -#: assets/models/asset/common.py:109 assets/serializers/asset/common.py:67 +#: assets/models/asset/common.py:114 msgid "Labels" msgstr "ラベル" -#: assets/models/asset/common.py:224 +#: assets/models/asset/common.py:284 msgid "Can refresh asset hardware info" msgstr "資産ハードウェア情報を更新できます" -#: assets/models/asset/common.py:225 +#: assets/models/asset/common.py:285 msgid "Can test asset connectivity" msgstr "資産接続をテストできます" -#: assets/models/asset/common.py:226 +#: assets/models/asset/common.py:286 #, fuzzy msgid "Can push account to asset" msgstr "システムユーザーを資産にプッシュできます" -#: assets/models/asset/common.py:227 +#: assets/models/asset/common.py:287 +#, fuzzy +msgid "Can verify account" +msgstr "パスワード/キーの確認" + +#: assets/models/asset/common.py:288 msgid "Can match asset" msgstr "アセットを一致させることができます" -#: assets/models/asset/common.py:228 +#: assets/models/asset/common.py:289 msgid "Add asset to node" msgstr "ノードにアセットを追加する" -#: assets/models/asset/common.py:229 +#: assets/models/asset/common.py:290 msgid "Move asset to node" msgstr "アセットをノードに移動する" -#: assets/models/asset/database.py:9 settings/serializers/email.py:37 +#: assets/models/asset/database.py:10 settings/serializers/email.py:37 msgid "Use SSL" msgstr "SSLの使用" -#: assets/models/asset/database.py:10 +#: assets/models/asset/database.py:11 #, fuzzy msgid "CA cert" msgstr "SP 証明書" -#: assets/models/asset/database.py:11 +#: assets/models/asset/database.py:12 #, fuzzy msgid "Client cert" msgstr "クライアント秘密" -#: assets/models/asset/database.py:12 +#: assets/models/asset/database.py:13 #, fuzzy msgid "Client key" msgstr "クライアント" -#: assets/models/asset/database.py:13 +#: assets/models/asset/database.py:14 msgid "Allow invalid cert" msgstr "証明書チェックを無視" -#: assets/models/asset/web.py:9 audits/const.py:68 -#: terminal/serializers/applet_host.py:25 +#: assets/models/asset/web.py:9 audits/const.py:39 +#: terminal/serializers/applet_host.py:27 msgid "Disabled" msgstr "無効" @@ -782,147 +1288,32 @@ msgid "Submit selector" msgstr "" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:69 +#: assets/serializers/asset/common.py:241 perms/models/asset_permission.py:70 #: perms/serializers/permission.py:32 rbac/tree.py:36 msgid "Accounts" msgstr "アカウント" -#: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:34 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -msgid "Assets" -msgstr "資産" - -#: assets/models/automations/base.py:81 assets/models/automations/base.py:88 +#: assets/models/automations/base.py:28 assets/models/automations/base.py:98 #, fuzzy msgid "Automation task" msgstr "自動管理" -#: assets/models/automations/base.py:90 audits/models.py:129 -#: audits/serializers.py:46 ops/models/base.py:49 ops/models/job.py:98 -#: terminal/models/applet/applet.py:130 terminal/models/applet/host.py:107 -#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 -#: tickets/models/ticket/general.py:283 tickets/serializers/ticket/ticket.py:20 -#: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:224 +#: assets/models/automations/base.py:91 +#, fuzzy +msgid "Asset automation task" +msgstr "自動管理" + +#: assets/models/automations/base.py:100 audits/models.py:135 +#: audits/serializers.py:48 ops/models/base.py:49 ops/models/job.py:99 +#: terminal/models/applet/applet.py:136 terminal/models/applet/host.py:107 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:17 +#: terminal/serializers/applet_host.py:90 tickets/models/ticket/general.py:283 +#: tickets/serializers/super_ticket.py:13 +#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/models.py:224 msgid "Status" msgstr "ステータス" -#: assets/models/automations/base.py:92 assets/models/backup.py:73 -#: audits/models.py:41 ops/models/base.py:55 ops/models/celery.py:60 -#: ops/models/job.py:106 perms/models/asset_permission.py:71 -#: terminal/models/applet/host.py:108 terminal/models/session/session.py:43 -#: tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:19 -msgid "Date start" -msgstr "開始日" - -#: assets/models/automations/base.py:93 -#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 -#: ops/models/celery.py:61 ops/models/job.py:107 -#: terminal/models/applet/host.py:109 -msgid "Date finished" -msgstr "終了日" - -#: assets/models/automations/base.py:95 -#: assets/serializers/automations/base.py:39 -#, fuzzy -msgid "Automation snapshot" -msgstr "製造オーダスナップショット" - -#: assets/models/automations/base.py:99 assets/models/backup.py:84 -#: assets/serializers/automations/base.py:41 -msgid "Trigger mode" -msgstr "トリガーモード" - -#: assets/models/automations/base.py:103 -#: assets/serializers/automations/change_secret.py:103 -#, fuzzy -msgid "Automation task execution" -msgstr "インスタンスタスクの同期実行" - -#: assets/models/automations/base.py:105 -msgid "Can view change secret execution" -msgstr "改密実行の表示" - -#: assets/models/automations/base.py:106 -msgid "Can add change secret execution" -msgstr "改密実行の作成" - -#: assets/models/automations/base.py:107 -msgid "Can view gather accounts execution" -msgstr "収集アカウント実行の表示" - -#: assets/models/automations/base.py:108 -msgid "Can add gather accounts execution" -msgstr "収集アカウントの作成実行" - -#: assets/models/automations/change_secret.py:15 assets/models/base.py:67 -#: assets/serializers/account/account.py:112 assets/serializers/base.py:13 -#: authentication/serializers/connect_token_secret.py:39 -#: authentication/serializers/connect_token_secret.py:49 -msgid "Secret type" -msgstr "鍵の種類" - -#: assets/models/automations/change_secret.py:19 -#: assets/serializers/automations/change_secret.py:25 -msgid "Secret strategy" -msgstr "鍵ポリシー" - -#: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:57 assets/models/base.py:69 -#: assets/serializers/base.py:16 authentication/models/temp_token.py:10 -#: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:19 -msgid "Secret" -msgstr "ひみつ" - -#: assets/models/automations/change_secret.py:22 -msgid "Password rules" -msgstr "パスワードルール" - -#: assets/models/automations/change_secret.py:25 -#, fuzzy -msgid "SSH key change strategy" -msgstr "SSHキー戦略" - -#: assets/models/automations/change_secret.py:27 assets/models/backup.py:25 -#: assets/serializers/account/backup.py:30 -#: assets/serializers/automations/change_secret.py:40 -msgid "Recipient" -msgstr "受信者" - -#: assets/models/automations/change_secret.py:34 -#, fuzzy -msgid "Change secret automation" -msgstr "セキュリティ設定を変更できます" - -#: assets/models/automations/change_secret.py:56 -#, fuzzy -msgid "Old secret" -msgstr "OTP 秘密" - -#: assets/models/automations/change_secret.py:58 -#, fuzzy -msgid "Date started" -msgstr "開始日" - -#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 -#, fuzzy -msgid "Error" -msgstr "企業微信エラー" - -#: assets/models/automations/change_secret.py:64 -#, fuzzy -msgid "Change secret record" -msgstr "パスワードの変更" - -#: assets/models/automations/gather_accounts.py:15 -#: assets/tasks/gather_accounts.py:28 -#, fuzzy -msgid "Gather asset accounts" -msgstr "アカウントを集める" - #: assets/models/automations/gather_facts.py:15 #, fuzzy msgid "Gather asset facts" @@ -933,59 +1324,15 @@ msgstr "資産ユーザーの収集" msgid "Ping asset" msgstr "ログイン資産" -#: assets/models/automations/push_account.py:16 -#, fuzzy -msgid "Push asset account" -msgstr "サービスアカウントです" - -#: assets/models/automations/verify_account.py:15 -#, fuzzy -msgid "Verify asset account" -msgstr "パスワード/キーの確認" - -#: assets/models/backup.py:34 assets/models/backup.py:92 -msgid "Account backup plan" -msgstr "アカウントバックアップ計画" - -#: assets/models/backup.py:76 -#: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:186 -msgid "Time" -msgstr "時間" - -#: assets/models/backup.py:80 -msgid "Account backup snapshot" -msgstr "アカウントのバックアップスナップショット" - -#: assets/models/backup.py:87 audits/models.py:124 -#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 -msgid "Reason" -msgstr "理由" - -#: assets/models/backup.py:89 -#: assets/serializers/automations/change_secret.py:99 -#: assets/serializers/automations/change_secret.py:124 -#: terminal/serializers/session.py:44 -msgid "Is success" -msgstr "成功は" - -#: assets/models/backup.py:96 -msgid "Account backup execution" -msgstr "アカウントバックアップの実行" - -#: assets/models/base.py:26 +#: assets/models/base.py:19 msgid "Connectivity" msgstr "接続性" -#: assets/models/base.py:28 authentication/models/temp_token.py:12 +#: assets/models/base.py:21 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "確認済みの日付" -#: assets/models/base.py:70 -msgid "Privileged" -msgstr "" - -#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:60 +#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 #: perms/serializers/permission.py:25 users/models/group.py:25 #: users/models/user.py:681 msgid "User group" @@ -1015,45 +1362,14 @@ msgstr "フィルター" msgid "Command filter rule" msgstr "コマンドフィルタルール" -#: assets/models/gateway.py:40 assets/serializers/domain.py:16 +#: assets/models/favorite_asset.py:17 +msgid "Favorite Asset" +msgstr "お気に入り" + +#: assets/models/gateway.py:35 assets/serializers/domain.py:16 msgid "Gateway" msgstr "ゲートウェイ" -#: assets/models/gateway.py:62 authentication/models/connection_token.py:104 -#, fuzzy -msgid "No account" -msgstr "アカウント" - -#: assets/models/gateway.py:84 -#, fuzzy, python-brace-format -msgid "Unable to connect to port {port} on {address}" -msgstr "{ip} でポート {port} に接続できません" - -#: assets/models/gateway.py:87 authentication/middleware.py:76 -#: xpack/plugins/cloud/providers/fc.py:48 -msgid "Authentication failed" -msgstr "認証に失敗しました" - -#: assets/models/gateway.py:89 assets/models/gateway.py:116 -msgid "Connect failed" -msgstr "接続に失敗しました" - -#: assets/models/gathered_user.py:14 -msgid "Present" -msgstr "プレゼント" - -#: assets/models/gathered_user.py:15 -msgid "Date last login" -msgstr "最終ログイン日" - -#: assets/models/gathered_user.py:16 -msgid "IP last login" -msgstr "IP最終ログイン" - -#: assets/models/gathered_user.py:27 -msgid "GatherUser" -msgstr "収集ユーザー" - #: assets/models/group.py:30 msgid "Asset group" msgstr "資産グループ" @@ -1075,14 +1391,14 @@ msgstr "システム" #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:25 #: authentication/serializers/connect_token_secret.py:114 -#: common/drf/serializers/common.py:82 settings/models.py:34 +#: common/serializers/common.py:82 settings/models.py:34 msgid "Value" msgstr "値" -#: assets/models/label.py:40 assets/serializers/cagegory.py:6 -#: assets/serializers/cagegory.py:13 +#: assets/models/label.py:40 assets/serializers/asset/common.py:123 +#: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: authentication/serializers/connect_token_secret.py:113 -#: common/drf/serializers/common.py:81 settings/serializers/sms.py:7 +#: common/serializers/common.py:81 settings/serializers/sms.py:7 msgid "Label" msgstr "ラベル" @@ -1094,7 +1410,7 @@ msgstr "新しいノード" msgid "empty" msgstr "空" -#: assets/models/node.py:551 perms/models/perm_node.py:27 +#: assets/models/node.py:551 perms/models/perm_node.py:28 msgid "Key" msgstr "キー" @@ -1102,12 +1418,12 @@ msgstr "キー" msgid "Full value" msgstr "フルバリュー" -#: assets/models/node.py:557 perms/models/perm_node.py:29 +#: assets/models/node.py:557 perms/models/perm_node.py:30 msgid "Parent key" msgstr "親キー" #: assets/models/node.py:566 perms/serializers/permission.py:28 -#: xpack/plugins/cloud/models.py:96 +#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "ノード" @@ -1125,8 +1441,8 @@ msgstr "MFAが必要" msgid "Setting" msgstr "設定" -#: assets/models/platform.py:41 audits/const.py:69 settings/models.py:37 -#: terminal/serializers/applet_host.py:26 +#: assets/models/platform.py:41 audits/const.py:40 settings/models.py:37 +#: terminal/serializers/applet_host.py:28 msgid "Enabled" msgstr "有効化" @@ -1143,181 +1459,116 @@ msgstr "MFA有効化" msgid "Ping method" msgstr "" -#: assets/models/platform.py:45 assets/models/platform.py:55 +#: assets/models/platform.py:45 assets/models/platform.py:58 #, fuzzy msgid "Gather facts enabled" msgstr "資産ユーザーの収集" -#: assets/models/platform.py:46 assets/models/platform.py:57 +#: assets/models/platform.py:46 assets/models/platform.py:60 #, fuzzy msgid "Gather facts method" msgstr "資産ユーザーの収集" #: assets/models/platform.py:47 #, fuzzy -msgid "Push account enabled" -msgstr "MFAが有効化されていません" - -#: assets/models/platform.py:48 -msgid "Push account method" -msgstr "" +msgid "Change secret enabled" +msgstr "パスワードの変更" #: assets/models/platform.py:49 #, fuzzy -msgid "Change password enabled" +msgid "Change secret method" msgstr "パスワードの変更" #: assets/models/platform.py:51 #, fuzzy -msgid "Change password method" -msgstr "パスワードの変更" +msgid "Push account enabled" +msgstr "MFAが有効化されていません" -#: assets/models/platform.py:52 +#: assets/models/platform.py:53 +#, fuzzy +msgid "Push account method" +msgstr "サービスアカウントです" + +#: assets/models/platform.py:55 #, fuzzy msgid "Verify account enabled" msgstr "サービスアカウントキー" -#: assets/models/platform.py:54 +#: assets/models/platform.py:57 #, fuzzy msgid "Verify account method" msgstr "パスワード/キーの確認" -#: assets/models/platform.py:74 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:77 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "メタ" -#: assets/models/platform.py:75 +#: assets/models/platform.py:78 msgid "Internal" msgstr "ビルトイン" -#: assets/models/platform.py:79 assets/serializers/platform.py:84 +#: assets/models/platform.py:82 assets/serializers/platform.py:84 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:81 +#: assets/models/platform.py:84 #, fuzzy msgid "Domain enabled" msgstr "ドメイン名" -#: assets/models/platform.py:82 +#: assets/models/platform.py:85 #, fuzzy msgid "Protocols enabled" msgstr "プロトコル" -#: assets/models/platform.py:84 +#: assets/models/platform.py:87 #, fuzzy msgid "Su enabled" msgstr "MFA有効化" -#: assets/models/platform.py:85 +#: assets/models/platform.py:88 #, fuzzy msgid "Su method" msgstr "接続タイムアウト" -#: assets/models/platform.py:87 assets/serializers/platform.py:91 +#: assets/models/platform.py:90 assets/serializers/platform.py:91 #, fuzzy msgid "Automation" msgstr "自動管理" -#: assets/models/utils.py:19 +#: assets/models/utils.py:18 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s は偶数ではありません" -#: assets/notifications.py:8 -msgid "Notification of account backup route task results" -msgstr "アカウントバックアップルートタスクの結果の通知" - -#: assets/notifications.py:18 -msgid "" -"{} - The account backup passage task has been completed. See the attachment " -"for details" -msgstr "" -"{} -アカウントバックアップの通過タスクが完了しました。詳細は添付ファイルをご" -"覧ください" - -#: assets/notifications.py:20 -msgid "" -"{} - The account backup passage task has been completed: the encryption " -"password has not been set - please go to personal information -> file " -"encryption password to set the encryption password" -msgstr "" -"{} -アカウントのバックアップ通過タスクが完了しました: 暗号化パスワードが設定" -"されていません-個人情報にアクセスしてください-> ファイル暗号化パスワードを設" -"定してください暗号化パスワード" - -#: assets/notifications.py:31 -msgid "Notification of implementation result of encryption change plan" -msgstr "暗号化変更プランの実装結果の通知" - -#: assets/notifications.py:41 -msgid "" -"{} - The encryption change task has been completed. See the attachment for " -"details" -msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください" - -#: assets/notifications.py:42 -msgid "" -"{} - The encryption change task has been completed: the encryption password " -"has not been set - please go to personal information -> file encryption " -"password to set the encryption password" -msgstr "" -"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" -"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" - -#: assets/serializers/account/account.py:18 -msgid "Push now" -msgstr "" - -#: assets/serializers/account/account.py:20 -#: assets/serializers/account/base.py:13 -#, fuzzy -msgid "Has secret" -msgstr "ひみつ" - -#: assets/serializers/account/account.py:27 -msgid "Account template not found" -msgstr "" - -#: assets/serializers/account/account.py:72 -#, fuzzy -#| msgid "Asset Info" -msgid "Asset not found" -msgstr "資産情報" - -#: assets/serializers/account/backup.py:29 -#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 -#: settings/serializers/auth/ldap.py:66 -msgid "Periodic perform" -msgstr "定期的なパフォーマンス" - -#: assets/serializers/account/backup.py:31 -#: assets/serializers/automations/change_secret.py:41 -msgid "Currently only mail sending is supported" -msgstr "現在、メール送信のみがサポートされています" - -#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:89 -#: authentication/serializers/connect_token_secret.py:27 +#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:89 +#: authentication/serializers/connect_token_secret.py:28 #: authentication/serializers/connect_token_secret.py:65 -#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:107 +#: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:107 #: xpack/plugins/cloud/serializers/task.py:38 msgid "Protocols" msgstr "プロトコル" -#: assets/serializers/asset/common.py:88 +#: assets/serializers/asset/common.py:126 +#, fuzzy +#| msgid "Enabled" +msgid "Enabled info" +msgstr "有効化" + +#: assets/serializers/asset/common.py:144 msgid "Address" msgstr "アドレス" -#: assets/serializers/asset/common.py:89 +#: assets/serializers/asset/common.py:145 msgid "Node path" msgstr "ノードパスです" -#: assets/serializers/asset/common.py:157 +#: assets/serializers/asset/common.py:205 #, fuzzy msgid "Platform not exist" msgstr "アプリが存在しません" -#: assets/serializers/asset/common.py:173 +#: assets/serializers/asset/common.py:221 #, fuzzy msgid "Protocol is required: {}" msgstr "プロトコル重複: {}" @@ -1383,35 +1634,6 @@ msgstr "ホスト名生" msgid "Asset number" msgstr "資産番号" -#: assets/serializers/automations/change_secret.py:28 -msgid "SSH Key strategy" -msgstr "SSHキー戦略" - -#: assets/serializers/automations/change_secret.py:70 -msgid "* Please enter the correct password length" -msgstr "* 正しいパスワードの長さを入力してください" - -#: assets/serializers/automations/change_secret.py:73 -msgid "* Password length range 6-30 bits" -msgstr "* パスワードの長さの範囲6-30ビット" - -#: assets/serializers/automations/change_secret.py:117 -#: assets/serializers/automations/change_secret.py:145 audits/const.py:74 -#: audits/models.py:40 common/const/choices.py:18 ops/const.py:50 -#: ops/serializers/celery.py:39 terminal/models/session/sharing.py:103 -#: tickets/views/approve.py:114 -msgid "Success" -msgstr "成功" - -#: assets/serializers/automations/gather_accounts.py:23 -#, fuzzy -msgid "Executed amount" -msgstr "実行時間" - -#: assets/serializers/base.py:21 -msgid "Key password" -msgstr "キーパスワード" - #: assets/serializers/cagegory.py:9 msgid "Constraints" msgstr "" @@ -1421,9 +1643,9 @@ msgstr "" msgid "Types" msgstr "タイプ" -#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:9 -msgid "Hostname" -msgstr "ホスト名" +#: assets/serializers/gateway.py:24 common/validators.py:32 +msgid "This field must be unique." +msgstr "このフィールドは一意である必要があります。" #: assets/serializers/label.py:12 msgid "Assets amount" @@ -1455,55 +1677,29 @@ msgstr "SFTPルート" msgid "Primary" msgstr "" -#: assets/serializers/utils.py:13 -msgid "Password can not contains `{{` " -msgstr "パスワードには '{{' を含まない" - -#: assets/serializers/utils.py:16 -msgid "Password can not contains `'` " -msgstr "パスワードには `'` を含まない" - -#: assets/serializers/utils.py:18 -msgid "Password can not contains `\"` " -msgstr "パスワードには `\"` を含まない" - -#: assets/serializers/utils.py:24 -msgid "private key invalid or passphrase error" -msgstr "秘密鍵が無効またはpassphraseエラー" - #: assets/tasks/automation.py:11 #, fuzzy -msgid "Execute automation" +msgid "Asset execute automation" msgstr "バッチ実行コマンド" -#: assets/tasks/backup.py:13 -#, fuzzy -msgid "Execute account backup plan" -msgstr "アカウントバックアップ計画" - -#: assets/tasks/gather_accounts.py:31 -#, fuzzy -msgid "Gather assets accounts" -msgstr "資産ユーザーの収集" - -#: assets/tasks/gather_facts.py:26 +#: assets/tasks/gather_facts.py:23 msgid "Update some assets hardware info. " msgstr "一部の資産ハードウェア情報を更新します。" -#: assets/tasks/gather_facts.py:44 +#: assets/tasks/gather_facts.py:53 #, fuzzy msgid "Manually update the hardware information of assets" msgstr "ノード資産のハードウェア情報を更新します。" -#: assets/tasks/gather_facts.py:49 +#: assets/tasks/gather_facts.py:57 msgid "Update assets hardware info: " msgstr "資産のハードウェア情報を更新する:" -#: assets/tasks/gather_facts.py:53 +#: assets/tasks/gather_facts.py:61 msgid "Manually update the hardware information of assets under a node" msgstr "" -#: assets/tasks/gather_facts.py:59 +#: assets/tasks/gather_facts.py:65 msgid "Update node asset hardware information: " msgstr "ノード資産のハードウェア情報を更新します。" @@ -1522,30 +1718,25 @@ msgstr "" msgid "Periodic check the amount of assets under the node" msgstr "" -#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 +#: assets/tasks/ping.py:37 assets/tasks/ping.py:54 #, fuzzy msgid "Test assets connectivity " msgstr "資産の接続性をテストします。" -#: assets/tasks/ping.py:33 +#: assets/tasks/ping.py:50 #, fuzzy -msgid "Manually test the connectivity of a asset" +msgid "Manually test the connectivity of a asset" msgstr "資産接続をテストできます" -#: assets/tasks/ping.py:43 +#: assets/tasks/ping.py:58 msgid "Manually test the connectivity of assets under a node" msgstr "" -#: assets/tasks/ping.py:49 +#: assets/tasks/ping.py:62 #, fuzzy msgid "Test if the assets under the node are connectable " msgstr "ノードの下のアセットが接続可能かどうかをテストします。" -#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:34 -#, fuzzy -msgid "Push accounts to assets" -msgstr "システムユーザーを資産にプッシュする:" - #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "資産が無効化されました。スキップ: {}" @@ -1562,15 +1753,6 @@ msgstr "セキュリティのために、ユーザー {} をプッシュしな msgid "No assets matched, stop task" msgstr "一致する資産がない、タスクを停止" -#: assets/tasks/verify_account.py:30 -msgid "Verify asset account availability" -msgstr "" - -#: assets/tasks/verify_account.py:37 -#, fuzzy -msgid "Verify accounts connectivity" -msgstr "テストアカウント接続:" - #: audits/apps.py:9 msgid "Audits" msgstr "監査" @@ -1579,78 +1761,93 @@ msgstr "監査" msgid "The text content is too long. Use Elasticsearch to store operation logs" msgstr "文章の内容が長すぎる。Elasticsearchで操作履歴を保存する" -#: audits/backends/db.py:24 audits/backends/db.py:26 +#: audits/backends/db.py:25 audits/backends/db.py:27 msgid "Tips" msgstr "謎々" -#: audits/const.py:45 +#: audits/const.py:12 msgid "Mkdir" msgstr "Mkdir" -#: audits/const.py:46 +#: audits/const.py:13 msgid "Rmdir" msgstr "Rmdir" -#: audits/const.py:47 audits/const.py:57 +#: audits/const.py:14 audits/const.py:24 #: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:232 +#: rbac/tree.py:231 msgid "Delete" msgstr "削除" -#: audits/const.py:48 perms/const.py:13 +#: audits/const.py:15 perms/const.py:13 msgid "Upload" msgstr "アップロード" -#: audits/const.py:49 +#: audits/const.py:16 msgid "Rename" msgstr "名前の変更" -#: audits/const.py:50 +#: audits/const.py:17 msgid "Symlink" msgstr "Symlink" -#: audits/const.py:51 perms/const.py:14 +#: audits/const.py:18 perms/const.py:14 msgid "Download" msgstr "ダウンロード" -#: audits/const.py:55 rbac/tree.py:230 +#: audits/const.py:22 rbac/tree.py:229 msgid "View" msgstr "表示" -#: audits/const.py:56 rbac/tree.py:231 templates/_csv_import_export.html:18 +#: audits/const.py:23 rbac/tree.py:230 templates/_csv_import_export.html:18 #: templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: audits/const.py:58 +#: audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:229 +#: rbac/tree.py:228 msgid "Create" msgstr "作成" -#: audits/const.py:63 settings/serializers/terminal.py:6 +#: audits/const.py:27 perms/const.py:12 +msgid "Connect" +msgstr "接続" + +#: audits/const.py:28 authentication/templates/authentication/login.html:254 +#: authentication/templates/authentication/login.html:327 +#: templates/_header_bar.html:89 +msgid "Login" +msgstr "ログイン" + +#: audits/const.py:29 ops/const.py:9 +#, fuzzy +msgid "Change password" +msgstr "パスワードの変更" + +#: audits/const.py:34 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:24 terminal/models/component/terminal.py:156 msgid "Terminal" msgstr "ターミナル" -#: audits/const.py:70 +#: audits/const.py:41 msgid "-" msgstr "-" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "Yes" msgstr "是" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "No" msgstr "否" -#: audits/models.py:32 audits/models.py:55 audits/models.py:96 +#: audits/models.py:32 audits/models.py:59 audits/models.py:102 #: terminal/models/session/session.py:37 terminal/models/session/sharing.py:95 msgid "Remote addr" msgstr "リモートaddr" -#: audits/models.py:37 audits/serializers.py:30 +#: audits/models.py:37 audits/serializers.py:32 msgid "Operate" msgstr "操作" @@ -1662,104 +1859,116 @@ msgstr "ファイル名" msgid "File transfer log" msgstr "ファイル転送ログ" -#: audits/models.py:53 audits/serializers.py:84 +#: audits/models.py:53 audits/serializers.py:86 msgid "Resource Type" msgstr "リソースタイプ" -#: audits/models.py:54 +#: audits/models.py:54 audits/models.py:57 msgid "Resource" msgstr "リソース" -#: audits/models.py:56 audits/models.py:98 -#: terminal/backends/command/serializers.py:40 +#: audits/models.py:60 audits/models.py:104 +#: terminal/backends/command/serializers.py:41 msgid "Datetime" msgstr "時間" -#: audits/models.py:88 +#: audits/models.py:63 +#, fuzzy +#| msgid "Is active" +msgid "Is Activity" +msgstr "アクティブです。" + +#: audits/models.py:93 msgid "Operate log" msgstr "ログの操作" -#: audits/models.py:94 +#: audits/models.py:100 msgid "Change by" msgstr "による変更" -#: audits/models.py:104 +#: audits/models.py:110 msgid "Password change log" msgstr "パスワード変更ログ" -#: audits/models.py:111 +#: audits/models.py:117 msgid "Login type" msgstr "ログインタイプ" -#: audits/models.py:113 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:119 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "ログインIP" -#: audits/models.py:115 +#: audits/models.py:121 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "ログイン都市" -#: audits/models.py:118 audits/serializers.py:60 +#: audits/models.py:124 audits/serializers.py:62 msgid "User agent" msgstr "ユーザーエージェント" -#: audits/models.py:121 audits/serializers.py:44 +#: audits/models.py:127 audits/serializers.py:46 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms/profile.py:65 users/models/user.py:698 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:131 +#: audits/models.py:137 msgid "Date login" msgstr "日付ログイン" -#: audits/models.py:133 audits/serializers.py:62 +#: audits/models.py:139 audits/serializers.py:64 msgid "Authentication backend" msgstr "認証バックエンド" -#: audits/models.py:174 +#: audits/models.py:180 msgid "User login log" msgstr "ユーザーログインログ" -#: audits/serializers.py:61 +#: audits/serializers.py:63 msgid "Reason display" msgstr "理由表示" -#: audits/signal_handlers.py:48 +#: audits/serializers.py:112 +#, fuzzy +#| msgid "User {} {} it." +msgid "User {} {} this resource." +msgstr "ユーザー {} はそれを {} しました" + +#: audits/signal_handlers.py:50 msgid "SSH Key" msgstr "SSHキー" -#: audits/signal_handlers.py:50 settings/serializers/auth/sso.py:10 +#: audits/signal_handlers.py:52 settings/serializers/auth/sso.py:10 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:53 msgid "Auth Token" msgstr "認証トークン" -#: audits/signal_handlers.py:52 authentication/notifications.py:73 -#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: audits/signal_handlers.py:54 authentication/notifications.py:73 +#: authentication/views/login.py:73 authentication/views/wecom.py:177 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:736 msgid "WeCom" msgstr "企業微信" -#: audits/signal_handlers.py:53 authentication/views/feishu.py:145 +#: audits/signal_handlers.py:55 authentication/views/feishu.py:144 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 users/models/user.py:738 msgid "FeiShu" msgstr "本を飛ばす" -#: audits/signal_handlers.py:54 authentication/views/dingtalk.py:180 +#: audits/signal_handlers.py:56 authentication/views/dingtalk.py:179 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:737 msgid "DingTalk" msgstr "DingTalk" -#: audits/signal_handlers.py:55 authentication/models/temp_token.py:16 +#: audits/signal_handlers.py:57 authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "仮パスワード" @@ -1767,6 +1976,26 @@ msgstr "仮パスワード" msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" +#: authentication/api/connection_token.py:264 +#, fuzzy +#| msgid "Asset Info" +msgid "Account not found" +msgstr "資産情報" + +#: authentication/api/connection_token.py:267 +#, fuzzy +#| msgid "Permission name" +msgid "Permission Expired" +msgstr "認可ルール名" + +#: authentication/api/connection_token.py:279 +msgid "ACL action is reject" +msgstr "" + +#: authentication/api/connection_token.py:283 +msgid "ACL action is review" +msgstr "" + #: authentication/api/mfa.py:59 msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" @@ -1800,7 +2029,7 @@ msgstr "パスワードを忘れた" #: authentication/apps.py:7 settings/serializers/auth/base.py:10 #: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 #: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 -#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/oauth2.py:18 settings/serializers/auth/oidc.py:12 #: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 #: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 msgid "Authentication" @@ -2001,21 +2230,21 @@ msgstr "電話が設定されていない" msgid "SSO auth closed" msgstr "SSO authは閉鎖されました" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:79 msgid "WeCom is already bound" msgstr "企業の微信はすでにバインドされています" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237 -#: authentication/views/wecom.py:291 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:236 +#: authentication/views/wecom.py:290 msgid "WeCom is not bound" msgstr "企業の微信をバインドしていません" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 -#: authentication/views/dingtalk.py:297 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 +#: authentication/views/dingtalk.py:296 msgid "DingTalk is not bound" msgstr "DingTalkはバインドされていません" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 msgid "FeiShu is not bound" msgstr "本を飛ばすは拘束されていません" @@ -2173,34 +2402,45 @@ msgid "Asset display" msgstr "アセット名" #: authentication/models/connection_token.py:41 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:73 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 #: tickets/models/ticket/apply_application.py:31 #: tickets/models/ticket/apply_asset.py:20 users/models/user.py:719 msgid "Date expired" msgstr "期限切れの日付" #: authentication/models/connection_token.py:45 +#: perms/models/asset_permission.py:77 +msgid "From ticket" +msgstr "チケットから" + +#: authentication/models/connection_token.py:51 msgid "Connection token" msgstr "接続トークン" -#: authentication/models/connection_token.py:47 +#: authentication/models/connection_token.py:53 msgid "Can view connection token secret" msgstr "接続トークンの秘密を表示できます" -#: authentication/models/connection_token.py:94 +#: authentication/models/connection_token.py:100 +#, fuzzy +#| msgid "Connection token" +msgid "Connection token inactive" +msgstr "接続トークン" + +#: authentication/models/connection_token.py:103 msgid "Connection token expired at: {}" msgstr "接続トークンの有効期限: {}" -#: authentication/models/connection_token.py:97 +#: authentication/models/connection_token.py:106 msgid "No user or invalid user" msgstr "" -#: authentication/models/connection_token.py:101 +#: authentication/models/connection_token.py:110 #, fuzzy msgid "No asset or inactive asset" msgstr "アセットがアクティブ化されていません" -#: authentication/models/connection_token.py:248 +#: authentication/models/connection_token.py:257 msgid "Super connection token" msgstr "スーパー接続トークン" @@ -2248,9 +2488,9 @@ msgstr "システムコンポーネント" msgid "Expired now" msgstr "期限切れ" -#: authentication/serializers/connect_token_secret.py:146 +#: authentication/serializers/connect_token_secret.py:147 #: authentication/templates/authentication/_access_key_modal.html:30 -#: perms/models/perm_node.py:20 users/serializers/group.py:35 +#: perms/models/perm_node.py:21 users/serializers/group.py:35 msgid "ID" msgstr "ID" @@ -2258,6 +2498,12 @@ msgstr "ID" msgid "Expired time" msgstr "期限切れ時間" +#: authentication/serializers/connection_token.py:18 +#, fuzzy +#| msgid "Ticket flow" +msgid "Ticket info" +msgstr "チケットの流れ" + #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 @@ -2274,7 +2520,7 @@ msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" #: authentication/serializers/token.py:79 perms/serializers/permission.py:30 -#: perms/serializers/permission.py:61 users/serializers/user.py:159 +#: perms/serializers/permission.py:62 users/serializers/user.py:159 msgid "Is valid" msgstr "有効です" @@ -2463,12 +2709,6 @@ msgstr "" msgid "Cancel" msgstr "キャンセル" -#: authentication/templates/authentication/login.html:254 -#: authentication/templates/authentication/login.html:327 -#: templates/_header_bar.html:89 -msgid "Login" -msgstr "ログイン" - #: authentication/templates/authentication/login.html:334 msgid "More login options" msgstr "その他のログインオプション" @@ -2510,73 +2750,73 @@ msgstr "コピー成功" msgid "LAN" msgstr "ローカルエリアネットワーク" -#: authentication/views/dingtalk.py:42 +#: authentication/views/dingtalk.py:41 msgid "DingTalk Error, Please contact your system administrator" msgstr "DingTalkエラー、システム管理者に連絡してください" -#: authentication/views/dingtalk.py:45 +#: authentication/views/dingtalk.py:44 msgid "DingTalk Error" msgstr "DingTalkエラー" -#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 -#: authentication/views/wecom.py:56 +#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:55 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "システム設定が正しくありません。管理者に連絡してください" -#: authentication/views/dingtalk.py:81 +#: authentication/views/dingtalk.py:80 msgid "DingTalk is already bound" msgstr "DingTalkはすでにバインドされています" -#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 +#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:147 msgid "Invalid user_id" msgstr "無効なuser_id" -#: authentication/views/dingtalk.py:165 +#: authentication/views/dingtalk.py:164 msgid "DingTalk query user failed" msgstr "DingTalkクエリユーザーが失敗しました" -#: authentication/views/dingtalk.py:174 +#: authentication/views/dingtalk.py:173 msgid "The DingTalk is already bound to another user" msgstr "DingTalkはすでに別のユーザーにバインドされています" -#: authentication/views/dingtalk.py:181 +#: authentication/views/dingtalk.py:180 msgid "Binding DingTalk successfully" msgstr "DingTalkのバインドに成功" -#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 +#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 msgid "Failed to get user from DingTalk" msgstr "DingTalkからユーザーを取得できませんでした" -#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 +#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 msgid "Please login with a password and then bind the DingTalk" msgstr "パスワードでログインし、DingTalkをバインドしてください" -#: authentication/views/feishu.py:40 +#: authentication/views/feishu.py:39 msgid "FeiShu Error" msgstr "FeiShuエラー" -#: authentication/views/feishu.py:88 +#: authentication/views/feishu.py:87 msgid "FeiShu is already bound" msgstr "FeiShuはすでにバインドされています" -#: authentication/views/feishu.py:130 +#: authentication/views/feishu.py:129 msgid "FeiShu query user failed" msgstr "FeiShuクエリユーザーが失敗しました" -#: authentication/views/feishu.py:139 +#: authentication/views/feishu.py:138 msgid "The FeiShu is already bound to another user" msgstr "FeiShuはすでに別のユーザーにバインドされています" -#: authentication/views/feishu.py:146 +#: authentication/views/feishu.py:145 msgid "Binding FeiShu successfully" msgstr "本を飛ばすのバインドに成功" -#: authentication/views/feishu.py:198 +#: authentication/views/feishu.py:197 msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" -#: authentication/views/feishu.py:205 +#: authentication/views/feishu.py:204 msgid "Please login with a password and then bind the FeiShu" msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" @@ -2612,34 +2852,38 @@ msgstr "ログアウト成功" msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" -#: authentication/views/wecom.py:41 +#: authentication/views/wecom.py:40 msgid "WeCom Error, Please contact your system administrator" msgstr "企業微信エラー、システム管理者に連絡してください" -#: authentication/views/wecom.py:44 +#: authentication/views/wecom.py:43 msgid "WeCom Error" msgstr "企業微信エラー" -#: authentication/views/wecom.py:163 +#: authentication/views/wecom.py:162 msgid "WeCom query user failed" msgstr "企業微信ユーザーの問合せに失敗しました" -#: authentication/views/wecom.py:172 +#: authentication/views/wecom.py:171 msgid "The WeCom is already bound to another user" msgstr "この企業の微信はすでに他のユーザーをバインドしている。" -#: authentication/views/wecom.py:179 +#: authentication/views/wecom.py:178 msgid "Binding WeCom successfully" msgstr "企業の微信のバインドに成功" -#: authentication/views/wecom.py:231 authentication/views/wecom.py:285 +#: authentication/views/wecom.py:230 authentication/views/wecom.py:284 msgid "Failed to get user from WeCom" msgstr "企業の微信からユーザーを取得できませんでした" -#: authentication/views/wecom.py:238 authentication/views/wecom.py:292 +#: authentication/views/wecom.py:237 authentication/views/wecom.py:291 msgid "Please login with a password and then bind the WeCom" msgstr "パスワードでログインしてからWeComをバインドしてください" +#: common/api/action.py:52 +msgid "Request file format may be wrong" +msgstr "リクエストファイルの形式が間違っている可能性があります" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -2662,11 +2906,12 @@ msgstr "タイミングトリガー" msgid "Ready" msgstr "の準備を" -#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:39 +#: common/const/choices.py:16 terminal/const.py:58 tickets/const.py:29 +#: tickets/const.py:39 msgid "Pending" msgstr "未定" -#: common/const/choices.py:17 ops/const.py:49 +#: common/const/choices.py:17 ops/const.py:50 msgid "Running" msgstr "" @@ -2679,67 +2924,55 @@ msgstr "キャンセル" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:94 +#: common/db/fields.py:97 msgid "Marshal dict data to char field" msgstr "チャーフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:98 +#: common/db/fields.py:101 msgid "Marshal dict data to text field" msgstr "テキストフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:110 +#: common/db/fields.py:113 msgid "Marshal list data to char field" msgstr "元帥リストデータをチャーフィールドに" -#: common/db/fields.py:114 +#: common/db/fields.py:117 msgid "Marshal list data to text field" msgstr "マーシャルリストデータをテキストフィールドに" -#: common/db/fields.py:118 +#: common/db/fields.py:121 msgid "Marshal data to char field" msgstr "チャーフィールドへのマーシャルデータ" -#: common/db/fields.py:122 +#: common/db/fields.py:125 msgid "Marshal data to text field" msgstr "テキストフィールドへのマーシャルデータ" -#: common/db/fields.py:164 +#: common/db/fields.py:167 msgid "Encrypt field using Secret Key" msgstr "Secret Keyを使用したフィールドの暗号化" -#: common/db/models.py:75 +#: common/db/mixins.py:32 +msgid "is discard" +msgstr "は破棄されます" + +#: common/db/mixins.py:33 +msgid "discard time" +msgstr "時間を捨てる" + +#: common/db/models.py:34 msgid "Updated by" msgstr "によって更新" -#: common/drf/exc_handlers.py:25 +#: common/db/validators.py:9 +msgid "Invalid port range, should be like and within {}-{}" +msgstr "" + +#: common/drf/exc_handlers.py:26 msgid "Object" msgstr "オブジェクト" -#: common/drf/fields.py:77 tickets/serializers/ticket/common.py:58 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "このフィールドは必須です。" - -#: common/drf/fields.py:78 -#, fuzzy, python-brace-format -msgid "Invalid pk \"{pk_value}\" - object does not exist." -msgstr "%s オブジェクトは存在しません。" - -#: common/drf/fields.py:79 -#, python-brace-format -msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "" - -#: common/drf/fields.py:141 -msgid "Invalid data type, should be list" -msgstr "" - -#: common/drf/fields.py:156 -#, fuzzy -msgid "Invalid choice: {}" -msgstr "無効なIP" - -#: common/drf/metadata.py:130 +#: common/drf/metadata.py:127 msgid "Organization ID" msgstr "組織 ID" @@ -2751,15 +2984,6 @@ msgstr "ファイルの内容がオーバーフローしました (最大長 '{} msgid "Parse file error: {}" msgstr "解析ファイルエラー: {}" -#: common/drf/serializers/common.py:86 -msgid "Children" -msgstr "" - -#: common/drf/serializers/common.py:94 -#, fuzzy -msgid "File" -msgstr "ファイル名" - #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -2789,31 +3013,6 @@ msgstr "このアクションでは、MFAの確認が必要です。" msgid "Unexpect error occur" msgstr "予期しないエラーが発生します" -#: common/mixins/api/action.py:52 -msgid "Request file format may be wrong" -msgstr "リクエストファイルの形式が間違っている可能性があります" - -#: common/mixins/models.py:32 -msgid "is discard" -msgstr "は破棄されます" - -#: common/mixins/models.py:33 -msgid "discard time" -msgstr "時間を捨てる" - -#: common/mixins/views.py:58 -msgid "Export all" -msgstr "すべてエクスポート" - -#: common/mixins/views.py:60 -msgid "Export only selected items" -msgstr "選択項目のみエクスポート" - -#: common/mixins/views.py:65 -#, python-format -msgid "Export filtered: %s" -msgstr "検索のエクスポート: %s" - #: common/plugins/es.py:28 msgid "Invalid elasticsearch config" msgstr "無効なElasticsearch構成" @@ -2878,6 +3077,39 @@ msgstr "確認コードが正しくありません" msgid "Please wait {} seconds before sending" msgstr "{} 秒待ってから送信してください" +#: common/serializers/common.py:86 +msgid "Children" +msgstr "" + +#: common/serializers/common.py:94 +#, fuzzy +msgid "File" +msgstr "ファイル名" + +#: common/serializers/fields.py:100 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "このフィールドは必須です。" + +#: common/serializers/fields.py:101 +#, fuzzy, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "%s オブジェクトは存在しません。" + +#: common/serializers/fields.py:102 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: common/serializers/fields.py:172 +msgid "Invalid data type, should be list" +msgstr "" + +#: common/serializers/fields.py:187 +#, fuzzy +msgid "Invalid choice: {}" +msgstr "無効なIP" + #: common/tasks.py:13 #, fuzzy msgid "Send email" @@ -2900,10 +3132,6 @@ msgstr "署名が無効です。" msgid "Special char not allowed" msgstr "特別なcharは許可されていません" -#: common/validators.py:32 -msgid "This field must be unique." -msgstr "このフィールドは一意である必要があります。" - #: common/validators.py:40 msgid "Should not contains special characters" msgstr "特殊文字を含むべきではない" @@ -2912,6 +3140,19 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" +#: common/views/mixins.py:57 +msgid "Export all" +msgstr "すべてエクスポート" + +#: common/views/mixins.py:59 +msgid "Export only selected items" +msgstr "選択項目のみエクスポート" + +#: common/views/mixins.py:64 +#, python-format +msgid "Export filtered: %s" +msgstr "検索のエクスポート: %s" + #: jumpserver/conf.py:415 msgid "Create account successfully" msgstr "アカウントを正常に作成" @@ -3002,7 +3243,7 @@ msgstr "" msgid "Waiting task start" msgstr "タスク開始待ち" -#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:55 +#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:56 msgid "App ops" msgstr "アプリ操作" @@ -3019,20 +3260,23 @@ msgstr "確認済み" msgid "Collect" msgstr "" -#: ops/const.py:9 -#, fuzzy -msgid "Change password" -msgstr "パスワードの変更" - #: ops/const.py:19 msgid "Custom password" msgstr "カスタムパスワード" +#: ops/const.py:20 +msgid "All assets use the same random password" +msgstr "すべての資産は同じランダムパスワードを使用します" + +#: ops/const.py:21 +msgid "All assets use different random password" +msgstr "すべての資産は異なるランダムパスワードを使用します" + #: ops/const.py:33 msgid "Adhoc" msgstr "コマンド#コマンド#" -#: ops/const.py:34 ops/models/job.py:31 +#: ops/const.py:34 ops/models/job.py:32 msgid "Playbook" msgstr "Playbook" @@ -3048,12 +3292,22 @@ msgstr "特権アカウント優先" msgid "Skip" msgstr "スキップ" -#: ops/const.py:45 ops/models/adhoc.py:20 +#: ops/const.py:45 #, fuzzy #| msgid "PowerShell" msgid "Powershell" msgstr "PowerShell" +#: ops/const.py:46 +msgid "Python" +msgstr "" + +#: ops/const.py:52 +#, fuzzy +#| msgid "Test timeout" +msgid "Timeout" +msgstr "テストタイムアウト" + #: ops/exception.py:6 msgid "no valid program entry found." msgstr "利用可能なプログラムポータルがありません" @@ -3083,26 +3337,26 @@ msgstr "{} から {} までの範囲" msgid "Require periodic or regularly perform setting" msgstr "定期的または定期的に設定を行う必要があります" -#: ops/models/adhoc.py:24 +#: ops/models/adhoc.py:23 msgid "Pattern" msgstr "パターン" -#: ops/models/adhoc.py:26 ops/models/job.py:28 +#: ops/models/adhoc.py:25 ops/models/job.py:29 msgid "Module" msgstr "モジュール" -#: ops/models/adhoc.py:27 ops/models/celery.py:55 ops/models/job.py:26 +#: ops/models/adhoc.py:26 ops/models/celery.py:58 ops/models/job.py:27 #: terminal/models/component/task.py:16 msgid "Args" msgstr "アルグ" -#: ops/models/adhoc.py:28 ops/models/base.py:16 ops/models/base.py:53 -#: ops/models/job.py:33 ops/models/job.py:104 ops/models/playbook.py:16 +#: ops/models/adhoc.py:27 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:34 ops/models/job.py:105 ops/models/playbook.py:16 #: terminal/models/session/sharing.py:23 msgid "Creator" msgstr "作成者" -#: ops/models/adhoc.py:46 +#: ops/models/adhoc.py:45 msgid "AdHoc" msgstr "タスクの各バージョン" @@ -3116,85 +3370,95 @@ msgstr "アカウントキー" msgid "Last execution" msgstr "コマンド実行" -#: ops/models/base.py:22 +#: ops/models/base.py:22 ops/serializers/job.py:16 #, fuzzy msgid "Date last run" msgstr "最終同期日" -#: ops/models/base.py:51 ops/models/job.py:102 +#: ops/models/base.py:51 ops/models/job.py:103 #: xpack/plugins/cloud/models.py:170 msgid "Result" msgstr "結果" -#: ops/models/base.py:52 ops/models/job.py:103 +#: ops/models/base.py:52 ops/models/job.py:104 msgid "Summary" msgstr "概要" +#: ops/models/celery.py:16 +#, fuzzy +msgid "Date last publish" +msgstr "終了日" + #: ops/models/celery.py:47 msgid "Celery Task" msgstr "Celery タスク#タスク#" -#: ops/models/celery.py:56 terminal/models/component/task.py:17 +#: ops/models/celery.py:50 +msgid "Can view task monitor" +msgstr "タスクモニターを表示できます" + +#: ops/models/celery.py:59 terminal/models/component/task.py:17 msgid "Kwargs" msgstr "クワーグ" -#: ops/models/celery.py:57 tickets/models/comment.py:13 -#: tickets/models/ticket/general.py:44 tickets/models/ticket/general.py:279 +#: ops/models/celery.py:60 tickets/models/comment.py:13 +#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279 +#: tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状態" -#: ops/models/celery.py:58 terminal/models/session/sharing.py:110 +#: ops/models/celery.py:61 terminal/models/session/sharing.py:110 #: tickets/const.py:25 msgid "Finished" msgstr "終了" -#: ops/models/celery.py:59 +#: ops/models/celery.py:62 #, fuzzy msgid "Date published" msgstr "終了日" -#: ops/models/celery.py:83 +#: ops/models/celery.py:86 msgid "Celery Task Execution" msgstr "Celery タスク実行" -#: ops/models/job.py:29 +#: ops/models/job.py:30 msgid "Chdir" msgstr "Chdir" -#: ops/models/job.py:30 +#: ops/models/job.py:31 msgid "Timeout (Seconds)" msgstr "タイムアウト(秒)" -#: ops/models/job.py:35 +#: ops/models/job.py:36 msgid "Runas" msgstr "Runas" -#: ops/models/job.py:37 +#: ops/models/job.py:38 msgid "Runas policy" msgstr "Runas ポリシー" -#: ops/models/job.py:38 +#: ops/models/job.py:39 msgid "Use Parameter Define" msgstr "パラメータ定義を使用する" -#: ops/models/job.py:39 +#: ops/models/job.py:40 msgid "Parameters define" msgstr "パラメータ定義" -#: ops/models/job.py:91 +#: ops/models/job.py:92 msgid "Job" msgstr "ジョブ#ジョブ#" -#: ops/models/job.py:101 +#: ops/models/job.py:102 msgid "Parameters" msgstr "パラメータ" -#: ops/models/job.py:300 +#: ops/models/job.py:311 msgid "Job Execution" msgstr "ジョブ実行" -#: ops/models/job.py:311 +#: ops/models/job.py:322 msgid "Job audit log" msgstr "ジョブ監査ログ" @@ -3231,14 +3495,20 @@ msgstr "{max_threshold} を超えるCPUロード: => {value}" msgid "Run after save" msgstr "システムユーザーの実行" -#: ops/serializers/job.py:43 +#: ops/serializers/job.py:52 #, fuzzy msgid "Job type" msgstr "Docタイプ" -#: ops/serializers/job.py:44 -msgid "Material" -msgstr "マテリアル" +#: ops/serializers/job.py:55 terminal/serializers/session.py:53 +msgid "Is finished" +msgstr "終了しました" + +#: ops/serializers/job.py:56 +#, fuzzy +#| msgid "Test timeout" +msgid "Time cost" +msgstr "テストタイムアウト" #: ops/signal_handlers.py:74 terminal/models/applet/host.py:111 #: terminal/models/component/task.py:24 @@ -3281,10 +3551,6 @@ msgstr "定期的なパフォーマンス" msgid "Task log" msgstr "タスクログ" -#: ops/utils.py:64 -msgid "Update task content: {}" -msgstr "タスク内容の更新: {}" - #: ops/variables.py:24 msgid "The current user`s username of JumpServer" msgstr "JumpServerの現在のユーザーのユーザー名" @@ -3321,18 +3587,18 @@ msgstr "ジョブのID" msgid "Name of the job" msgstr "ジョブの名前" -#: orgs/api.py:67 +#: orgs/api.py:63 msgid "The current organization ({}) cannot be deleted" msgstr "現在の組織 ({}) は削除できません" -#: orgs/api.py:72 +#: orgs/api.py:68 msgid "" "LDAP synchronization is set to the current organization. Please switch to " "another organization before deleting" msgstr "" "LDAP 同期は現在の組織に設定されます。削除する前に別の組織に切り替えてください" -#: orgs/api.py:81 +#: orgs/api.py:78 msgid "The organization have resource ({}) cannot be deleted" msgstr "組織のリソース ({}) は削除できません" @@ -3340,10 +3606,10 @@ msgstr "組織のリソース ({}) は削除できません" msgid "App organizations" msgstr "アプリ組織" -#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:82 +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:84 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 -#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:62 +#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60 msgid "Organization" msgstr "組織" @@ -3351,28 +3617,28 @@ msgstr "組織" msgid "Org name" msgstr "組織名" -#: orgs/models.py:68 rbac/models/role.py:36 terminal/models/applet/applet.py:28 +#: orgs/models.py:70 rbac/models/role.py:36 terminal/models/applet/applet.py:29 #, fuzzy msgid "Builtin" msgstr "内蔵" -#: orgs/models.py:74 +#: orgs/models.py:76 msgid "GLOBAL" msgstr "グローバル組織" -#: orgs/models.py:76 +#: orgs/models.py:78 msgid "DEFAULT" msgstr "デフォルト組織" -#: orgs/models.py:78 +#: orgs/models.py:80 msgid "SYSTEM" msgstr "システム組織" -#: orgs/models.py:84 +#: orgs/models.py:86 msgid "Can view root org" msgstr "グローバル組織を表示できます" -#: orgs/models.py:85 +#: orgs/models.py:87 msgid "Can view all joined org" msgstr "参加しているすべての組織を表示できます" @@ -3385,10 +3651,6 @@ msgstr "グローバル組織名" msgid "App permissions" msgstr "アプリの権限" -#: perms/const.py:12 -msgid "Connect" -msgstr "接続" - #: perms/const.py:15 #, fuzzy msgid "Copy" @@ -3407,46 +3669,42 @@ msgstr "転送" msgid "Clipboard" msgstr "クリップボードのコピー" -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:29 -#: perms/serializers/permission.py:59 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:29 +#: perms/serializers/permission.py:60 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "アクション" -#: perms/models/asset_permission.py:76 -msgid "From ticket" -msgstr "チケットから" - -#: perms/models/asset_permission.py:82 +#: perms/models/asset_permission.py:83 msgid "Asset permission" msgstr "資産権限" -#: perms/models/perm_node.py:67 +#: perms/models/perm_node.py:68 msgid "Ungrouped" msgstr "グループ化されていません" -#: perms/models/perm_node.py:69 +#: perms/models/perm_node.py:70 msgid "Favorite" msgstr "お気に入り" -#: perms/models/perm_node.py:120 +#: perms/models/perm_node.py:121 msgid "Permed asset" msgstr "許可された資産" -#: perms/models/perm_node.py:122 +#: perms/models/perm_node.py:123 msgid "Can view my assets" msgstr "私の資産を見ることができます" -#: perms/models/perm_node.py:123 +#: perms/models/perm_node.py:124 msgid "Can view user assets" msgstr "ユーザー資産を表示できます" -#: perms/models/perm_node.py:124 +#: perms/models/perm_node.py:125 msgid "Can view usergroup assets" msgstr "ユーザーグループの資産を表示できます" -#: perms/models/perm_node.py:135 +#: perms/models/perm_node.py:136 #, fuzzy msgid "Permed account" msgstr "アカウントを集める" @@ -3471,7 +3729,7 @@ msgstr "資産権限の有効期限が近づいています" msgid "asset permissions of organization {}" msgstr "組織 {} の資産権限" -#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: perms/serializers/permission.py:31 perms/serializers/permission.py:61 #: users/serializers/user.py:91 users/serializers/user.py:161 msgid "Is expired" msgstr "期限切れです" @@ -3512,27 +3770,27 @@ msgstr "{} 少なくとも1つのシステムロール" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:111 +#: rbac/builtin.py:109 msgid "SystemAdmin" msgstr "システム管理者" -#: rbac/builtin.py:114 +#: rbac/builtin.py:112 msgid "SystemAuditor" msgstr "システム監査人" -#: rbac/builtin.py:117 +#: rbac/builtin.py:115 msgid "SystemComponent" msgstr "システムコンポーネント" -#: rbac/builtin.py:123 +#: rbac/builtin.py:121 msgid "OrgAdmin" msgstr "組織管理者" -#: rbac/builtin.py:126 +#: rbac/builtin.py:124 msgid "OrgAuditor" msgstr "監査員を組織する" -#: rbac/builtin.py:129 +#: rbac/builtin.py:127 msgid "OrgUser" msgstr "組織ユーザー" @@ -3565,7 +3823,7 @@ msgid "Permissions" msgstr "権限" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 -#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 +#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:36 msgid "Scope" msgstr "スコープ" @@ -3613,7 +3871,7 @@ msgstr "パーマ" msgid "Users amount" msgstr "ユーザー数" -#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:23 +#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:24 msgid "Display name" msgstr "表示名" @@ -3665,24 +3923,24 @@ msgstr "バックアップアカウント" msgid "Gather account" msgstr "アカウントを集める" -#: rbac/tree.py:51 +#: rbac/tree.py:52 msgid "Asset change auth" msgstr "資産の改ざん" -#: rbac/tree.py:52 +#: rbac/tree.py:53 msgid "Terminal setting" msgstr "ターミナル設定" -#: rbac/tree.py:53 +#: rbac/tree.py:54 msgid "Task Center" msgstr "タスクセンター" -#: rbac/tree.py:54 +#: rbac/tree.py:55 msgid "My assets" msgstr "私の資産" -#: rbac/tree.py:56 terminal/models/applet/applet.py:38 -#: terminal/models/applet/applet.py:127 terminal/models/applet/host.py:27 +#: rbac/tree.py:57 terminal/models/applet/applet.py:39 +#: terminal/models/applet/applet.py:133 terminal/models/applet/host.py:27 msgid "Applet" msgstr "リモートアプリケーション" @@ -3702,10 +3960,6 @@ msgstr "共通設定" msgid "View permission tree" msgstr "権限ツリーの表示" -#: rbac/tree.py:124 -msgid "Execute batch command" -msgstr "バッチ実行コマンド" - #: settings/api/dingtalk.py:31 settings/api/feishu.py:36 #: settings/api/sms.py:148 settings/api/wecom.py:37 msgid "Test success" @@ -3847,7 +4101,7 @@ msgstr "サービス側アドレス" msgid "Proxy server url" msgstr "コールバックアドレス" -#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:54 #: settings/serializers/auth/saml2.py:34 msgid "Logout completely" msgstr "同期ログアウト" @@ -3909,7 +4163,7 @@ msgstr "ユーザー検索フィルター" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "選択は (cnまたはuidまたはsAMAccountName)=%(user)s)" -#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:56 #: settings/serializers/auth/oidc.py:37 msgid "User attr map" msgstr "ユーザー属性マッピング" @@ -3934,52 +4188,52 @@ msgstr "ページサイズを検索" msgid "Enable LDAP auth" msgstr "LDAP認証の有効化" -#: settings/serializers/auth/oauth2.py:19 +#: settings/serializers/auth/oauth2.py:18 msgid "OAuth2" msgstr "OAuth2" -#: settings/serializers/auth/oauth2.py:22 +#: settings/serializers/auth/oauth2.py:21 msgid "Enable OAuth2 Auth" msgstr "OAuth2認証の有効化" -#: settings/serializers/auth/oauth2.py:25 +#: settings/serializers/auth/oauth2.py:24 msgid "Logo" msgstr "アイコン" -#: settings/serializers/auth/oauth2.py:28 +#: settings/serializers/auth/oauth2.py:27 msgid "Service provider" msgstr "サービスプロバイダー" -#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 +#: settings/serializers/auth/oauth2.py:30 settings/serializers/auth/oidc.py:19 msgid "Client Id" msgstr "クライアントID" -#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 +#: settings/serializers/auth/oauth2.py:33 settings/serializers/auth/oidc.py:22 #: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "クライアント秘密" -#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:68 +#: settings/serializers/auth/oauth2.py:39 settings/serializers/auth/oidc.py:68 msgid "Provider auth endpoint" msgstr "認証エンドポイントアドレス" -#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:71 +#: settings/serializers/auth/oauth2.py:42 settings/serializers/auth/oidc.py:71 msgid "Provider token endpoint" msgstr "プロバイダートークンエンドポイント" -#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 +#: settings/serializers/auth/oauth2.py:45 settings/serializers/auth/oidc.py:30 msgid "Client authentication method" msgstr "クライアント認証方式" -#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:77 +#: settings/serializers/auth/oauth2.py:49 settings/serializers/auth/oidc.py:77 msgid "Provider userinfo endpoint" msgstr "プロバイダーuserinfoエンドポイント" -#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:80 +#: settings/serializers/auth/oauth2.py:52 settings/serializers/auth/oidc.py:80 msgid "Provider end session endpoint" msgstr "プロバイダーのセッション終了エンドポイント" -#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:98 +#: settings/serializers/auth/oauth2.py:59 settings/serializers/auth/oidc.py:98 #: settings/serializers/auth/saml2.py:35 msgid "Always update user" msgstr "常にユーザーを更新" @@ -4117,7 +4371,7 @@ msgstr "SMSプロバイダ / プロトコル" #: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 #: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 -#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:69 msgid "Signature" msgstr "署名" @@ -4191,7 +4445,7 @@ msgstr "" msgid "SSO auth key TTL" msgstr "Token有効期間" -#: settings/serializers/auth/sso.py:17 +#: settings/serializers/auth/sso.py:17 settings/serializers/security.py:117 #: xpack/plugins/cloud/serializers/account_attrs.py:176 msgid "Unit: second" msgstr "単位: 秒" @@ -4373,7 +4627,7 @@ msgstr "" msgid "Create user email content" msgstr "ユーザーのメールコンテンツを作成する" -#: settings/serializers/email.py:65 +#: settings/serializers/email.py:66 #, python-brace-format msgid "" "Tips: When creating a user, send the content of the email, support " @@ -4382,7 +4636,7 @@ msgstr "" "ヒント:ユーザーの作成時にパスワード設定メールの内容を送信し、{username}{name}" "{email}ラベルをサポートします。" -#: settings/serializers/email.py:69 +#: settings/serializers/email.py:70 msgid "Tips: Email signature (eg:jumpserver)" msgstr "ヒント: メール署名 (例: jumpserver)" @@ -4608,10 +4862,16 @@ msgstr "" "ます。" #: settings/serializers/security.py:116 +#, fuzzy +#| msgid "Verify code" +msgid "Verify code TTL" +msgstr "コードの確認" + +#: settings/serializers/security.py:121 msgid "Enable Login dynamic code" msgstr "ログイン動的コードの有効化" -#: settings/serializers/security.py:117 +#: settings/serializers/security.py:122 msgid "" "The password and additional code are sent to a third party authentication " "system for verification" @@ -4619,32 +4879,32 @@ msgstr "" "パスワードと追加コードは、検証のためにサードパーティの認証システムに送信され" "ます" -#: settings/serializers/security.py:122 +#: settings/serializers/security.py:127 msgid "MFA in login page" msgstr "ログインページのMFA" -#: settings/serializers/security.py:123 +#: settings/serializers/security.py:128 msgid "Eu security regulations(GDPR) require MFA to be on the login page" msgstr "" "Euセキュリティ規制 (GDPR) では、MFAがログインページにある必要があります" -#: settings/serializers/security.py:126 +#: settings/serializers/security.py:131 msgid "Enable Login captcha" msgstr "ログインcaptchaの有効化" -#: settings/serializers/security.py:127 +#: settings/serializers/security.py:132 msgid "Enable captcha to prevent robot authentication" msgstr "Captchaを有効にしてロボット認証を防止する" -#: settings/serializers/security.py:146 +#: settings/serializers/security.py:151 msgid "Security" msgstr "セキュリティ" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:154 msgid "Enable terminal register" msgstr "ターミナルレジスタの有効化" -#: settings/serializers/security.py:151 +#: settings/serializers/security.py:156 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" @@ -4652,64 +4912,64 @@ msgstr "" "ターミナルレジスタを許可し、すべてのターミナルセットアップの後、セキュリティ" "のためにこれを無効にする必要があります" -#: settings/serializers/security.py:155 +#: settings/serializers/security.py:160 msgid "Enable watermark" msgstr "透かしの有効化" -#: settings/serializers/security.py:156 +#: settings/serializers/security.py:161 msgid "Enabled, the web session and replay contains watermark information" msgstr "Webセッションとリプレイには透かし情報が含まれています。" -#: settings/serializers/security.py:160 +#: settings/serializers/security.py:165 msgid "Connection max idle time" msgstr "接続最大アイドル時間" -#: settings/serializers/security.py:161 +#: settings/serializers/security.py:166 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "アイドル時間がそれ以上の場合は、接続単位を切断します: 分" -#: settings/serializers/security.py:164 +#: settings/serializers/security.py:169 msgid "Remember manual auth" msgstr "手動入力パスワードの保存" -#: settings/serializers/security.py:167 +#: settings/serializers/security.py:172 msgid "Enable change auth secure mode" msgstr "安全モードの変更を有効にする" -#: settings/serializers/security.py:170 +#: settings/serializers/security.py:175 msgid "Insecure command alert" msgstr "安全でないコマンドアラート" -#: settings/serializers/security.py:173 +#: settings/serializers/security.py:178 msgid "Email recipient" msgstr "メール受信者" -#: settings/serializers/security.py:174 +#: settings/serializers/security.py:179 msgid "Multiple user using , split" msgstr "複数のユーザーを使用して、分割" -#: settings/serializers/security.py:177 -msgid "Batch command execution" -msgstr "バッチコマンドの実行" +#: settings/serializers/security.py:182 +msgid "Operation center" +msgstr "職業センター" -#: settings/serializers/security.py:178 +#: settings/serializers/security.py:183 msgid "Allow user run batch command or not using ansible" msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない" -#: settings/serializers/security.py:181 +#: settings/serializers/security.py:186 msgid "Session share" msgstr "セッション共有" -#: settings/serializers/security.py:182 +#: settings/serializers/security.py:187 msgid "Enabled, Allows user active session to be shared with other users" msgstr "" "ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。" -#: settings/serializers/security.py:185 +#: settings/serializers/security.py:190 msgid "Remote Login Protection" msgstr "リモートログイン保護" -#: settings/serializers/security.py:187 +#: settings/serializers/security.py:192 msgid "" "The system determines whether the login IP address belongs to a common login " "city. If the account is logged in from a common login city, the system sends " @@ -4719,6 +4979,10 @@ msgstr "" "します。アカウントが共通のログイン都市からログインしている場合、システムはリ" "モートログインリマインダーを送信します" +#: settings/serializers/terminal.py:9 +msgid "Hostname" +msgstr "ホスト名" + #: settings/serializers/terminal.py:15 msgid "Auto" msgstr "自動" @@ -5161,7 +5425,7 @@ msgid "Input" msgstr "入力" #: terminal/backends/command/models.py:24 -#: terminal/backends/command/serializers.py:38 +#: terminal/backends/command/serializers.py:39 msgid "Output" msgstr "出力" @@ -5181,23 +5445,23 @@ msgstr "リスクレベル" msgid "Session ID" msgstr "セッションID" -#: terminal/backends/command/serializers.py:37 +#: terminal/backends/command/serializers.py:38 #, fuzzy msgid "Account " msgstr "アカウント" -#: terminal/backends/command/serializers.py:39 +#: terminal/backends/command/serializers.py:40 msgid "Timestamp" msgstr "タイムスタンプ" -#: terminal/backends/command/serializers.py:41 +#: terminal/backends/command/serializers.py:42 #: terminal/models/component/terminal.py:84 msgid "Remote Address" msgstr "リモートアドレス" -#: terminal/connect_methods.py:46 terminal/connect_methods.py:47 -#: terminal/connect_methods.py:48 terminal/connect_methods.py:49 -#: terminal/connect_methods.py:50 +#: terminal/connect_methods.py:47 terminal/connect_methods.py:48 +#: terminal/connect_methods.py:49 terminal/connect_methods.py:50 +#: terminal/connect_methods.py:51 #, fuzzy msgid "DB Client" msgstr "クライアント" @@ -5219,6 +5483,10 @@ msgstr "正常" msgid "Offline" msgstr "オフライン" +#: terminal/const.py:61 +msgid "Mismatch" +msgstr "" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "一括作成非サポート" @@ -5227,20 +5495,26 @@ msgstr "一括作成非サポート" msgid "Storage is invalid" msgstr "ストレージが無効です" -#: terminal/models/applet/applet.py:25 +#: terminal/models/applet/applet.py:26 #, fuzzy msgid "Author" msgstr "資産アカウント" -#: terminal/models/applet/applet.py:30 +#: terminal/models/applet/applet.py:31 msgid "Tags" msgstr "" -#: terminal/models/applet/applet.py:34 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:35 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "ホスト" -#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 +#: terminal/models/applet/applet.py:135 terminal/models/applet/host.py:33 +#: terminal/models/applet/host.py:105 +#, fuzzy +msgid "Hosting" +msgstr "ホスト" + +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:40 #, fuzzy msgid "Deploy options" msgstr "その他のログインオプション" @@ -5259,48 +5533,61 @@ msgstr "終了日" msgid "Date synced" msgstr "日付の同期" -#: terminal/models/applet/host.py:33 -msgid "Applet host" -msgstr "リモートアプリケーションパブリッシャ" - -#: terminal/models/applet/host.py:105 -#, fuzzy -msgid "Hosting" -msgstr "ホスト" - #: terminal/models/applet/host.py:106 msgid "Initial" msgstr "" #: terminal/models/component/endpoint.py:15 -msgid "HTTPS Port" +msgid "HTTPS port" msgstr "HTTPS ポート" #: terminal/models/component/endpoint.py:16 -msgid "HTTP Port" +msgid "HTTP port" msgstr "HTTP ポート" #: terminal/models/component/endpoint.py:17 -msgid "SSH Port" +msgid "SSH port" msgstr "SSH ポート" #: terminal/models/component/endpoint.py:18 -msgid "RDP Port" +msgid "RDP port" msgstr "RDP ポート" -#: terminal/models/component/endpoint.py:25 -#: terminal/models/component/endpoint.py:94 terminal/serializers/endpoint.py:57 +#: terminal/models/component/endpoint.py:19 +#, fuzzy +#| msgid "SSH port" +msgid "MySQL port" +msgstr "SSH ポート" + +#: terminal/models/component/endpoint.py:20 +#, fuzzy +#| msgid "RDP port" +msgid "MariaDB port" +msgstr "RDP ポート" + +#: terminal/models/component/endpoint.py:21 +msgid "PostgreSQL port" +msgstr "" + +#: terminal/models/component/endpoint.py:22 +#, fuzzy +#| msgid "Test port" +msgid "Redis port" +msgstr "テストポート" + +#: terminal/models/component/endpoint.py:29 +#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64 #: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 #: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 #: terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "エンドポイント" -#: terminal/models/component/endpoint.py:87 +#: terminal/models/component/endpoint.py:91 msgid "IP group" msgstr "IP グループ" -#: terminal/models/component/endpoint.py:99 +#: terminal/models/component/endpoint.py:103 msgid "Endpoint rule" msgstr "エンドポイントルール" @@ -5466,78 +5753,65 @@ msgstr "レベル" msgid "Batch danger command alert" msgstr "一括危険コマンド警告" -#: terminal/serializers/applet.py:16 -#, fuzzy -msgid "Published" -msgstr "公開キー" - -#: terminal/serializers/applet.py:17 -#, fuzzy -msgid "Unpublished" -msgstr "終了" - -#: terminal/serializers/applet.py:18 -#, fuzzy -msgid "Not match" -msgstr "ユーザーにマッチしなかった" - -#: terminal/serializers/applet.py:32 +#: terminal/serializers/applet.py:27 msgid "Icon" msgstr "" -#: terminal/serializers/applet_host.py:21 +#: terminal/serializers/applet_host.py:23 #, fuzzy msgid "Per Session" msgstr "セッション" -#: terminal/serializers/applet_host.py:22 +#: terminal/serializers/applet_host.py:24 msgid "Per Device" msgstr "" -#: terminal/serializers/applet_host.py:28 +#: terminal/serializers/applet_host.py:30 #, fuzzy msgid "RDS Licensing" msgstr "ライセンス" -#: terminal/serializers/applet_host.py:29 +#: terminal/serializers/applet_host.py:31 msgid "RDS License Server" msgstr "" -#: terminal/serializers/applet_host.py:30 +#: terminal/serializers/applet_host.py:32 msgid "RDS Licensing Mode" msgstr "" -#: terminal/serializers/applet_host.py:32 +#: terminal/serializers/applet_host.py:34 msgid "RDS fSingleSessionPerUser" msgstr "" -#: terminal/serializers/applet_host.py:33 +#: terminal/serializers/applet_host.py:35 msgid "RDS Max Disconnection Time" msgstr "" -#: terminal/serializers/applet_host.py:34 +#: terminal/serializers/applet_host.py:36 msgid "RDS Remote App Logoff Time Limit" msgstr "" -#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +#: terminal/serializers/applet_host.py:42 terminal/serializers/terminal.py:41 msgid "Load status" msgstr "ロードステータス" #: terminal/serializers/endpoint.py:14 -msgid "Magnus listen db port" -msgstr "Magnus がリッスンするデータベース ポート" +msgid "Oracle port" +msgstr "Oracle ポート" #: terminal/serializers/endpoint.py:17 -msgid "Magnus Listen port range" -msgstr "Magnus がリッスンするポート範囲" +msgid "Oracle port range" +msgstr "Oracle がリッスンするポート範囲" #: terminal/serializers/endpoint.py:19 msgid "" -"The range of ports that Magnus listens on is modified in the configuration " -"file" -msgstr "Magnus がリッスンするポート範囲を構成ファイルで変更してください" +"Oracle proxy server listen port is dynamic, Each additional Oracle database " +"instance adds a port listener" +msgstr "" +"Oracle プロキシサーバーがリッスンするポートは動的です。追加の Oracle データ" +"ベースインスタンスはポートリスナーを追加します" -#: terminal/serializers/endpoint.py:51 +#: terminal/serializers/endpoint.py:58 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" msgstr "" @@ -5548,43 +5822,39 @@ msgstr "" msgid "Tunnel" msgstr "" -#: terminal/serializers/session.py:41 -msgid "User ID" -msgstr "ユーザーID" - -#: terminal/serializers/session.py:42 -msgid "Asset ID" -msgstr "資産ID" - -#: terminal/serializers/session.py:43 -msgid "Login from display" -msgstr "表示からのログイン" - -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:28 terminal/serializers/session.py:50 msgid "Can replay" msgstr "再生できます" -#: terminal/serializers/session.py:46 +#: terminal/serializers/session.py:29 terminal/serializers/session.py:51 msgid "Can join" msgstr "参加できます" -#: terminal/serializers/session.py:47 -msgid "Terminal ID" -msgstr "ターミナル ID" - -#: terminal/serializers/session.py:48 -msgid "Is finished" -msgstr "終了しました" - -#: terminal/serializers/session.py:49 +#: terminal/serializers/session.py:30 terminal/serializers/session.py:54 msgid "Can terminate" msgstr "終了できます" -#: terminal/serializers/session.py:50 +#: terminal/serializers/session.py:46 +msgid "User ID" +msgstr "ユーザーID" + +#: terminal/serializers/session.py:47 +msgid "Asset ID" +msgstr "資産ID" + +#: terminal/serializers/session.py:48 +msgid "Login from display" +msgstr "表示からのログイン" + +#: terminal/serializers/session.py:52 +msgid "Terminal ID" +msgstr "ターミナル ID" + +#: terminal/serializers/session.py:55 msgid "Terminal display" msgstr "ターミナルディスプレイ" -#: terminal/serializers/session.py:55 +#: terminal/serializers/session.py:60 msgid "Command amount" msgstr "コマンド量" @@ -5662,7 +5932,7 @@ msgstr "見つかりません" msgid "view" msgstr "表示" -#: terminal/utils/db_port_mapper.py:77 +#: terminal/utils/db_port_mapper.py:84 msgid "" "No available port is matched. The number of databases may have exceeded the " "number of ports open to the database agent service, Contact the " @@ -5672,7 +5942,7 @@ msgstr "" "サービスによって開かれたポートの数を超えた可能性があります。さらにポートを開" "くには、管理者に連絡してください。" -#: terminal/utils/db_port_mapper.py:103 +#: terminal/utils/db_port_mapper.py:112 msgid "" "No ports can be used, check and modify the limit on the number of ports that " "Magnus listens on in the configuration file." @@ -5680,7 +5950,7 @@ msgstr "" "使用できるポートがありません。設定ファイルで Magnus がリッスンするポート数の" "制限を確認して変更してください. " -#: terminal/utils/db_port_mapper.py:105 +#: terminal/utils/db_port_mapper.py:114 msgid "All available port count: {}, Already use port count: {}" msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" @@ -5781,15 +6051,15 @@ msgid "Body" msgstr "ボディ" #: tickets/models/flow.py:19 tickets/models/flow.py:61 -#: tickets/models/ticket/general.py:40 +#: tickets/models/ticket/general.py:41 msgid "Approve level" msgstr "レベルを承認する" -#: tickets/models/flow.py:24 tickets/serializers/flow.py:18 +#: tickets/models/flow.py:24 tickets/serializers/flow.py:17 msgid "Approve strategy" msgstr "戦略を承認する" -#: tickets/models/flow.py:29 tickets/serializers/flow.py:20 +#: tickets/models/flow.py:29 tickets/serializers/flow.py:19 msgid "Assignees" msgstr "アシニーズ" @@ -5823,21 +6093,15 @@ msgstr "システムユーザーの適用" msgid "Select at least one asset or node" msgstr "少なくとも1つのアセットまたはノードを選択します。" -#: tickets/models/ticket/apply_asset.py:14 -#: tickets/serializers/ticket/apply_asset.py:26 -msgid "Apply nodes" -msgstr "ノードの適用" - -#: tickets/models/ticket/apply_asset.py:16 -#: tickets/serializers/ticket/apply_asset.py:22 -msgid "Apply assets" -msgstr "申請資産" - #: tickets/models/ticket/apply_asset.py:17 #, fuzzy msgid "Apply accounts" msgstr "アプリケーションアカウント" +#: tickets/models/ticket/apply_asset.py:26 +msgid "Apply Asset Ticket" +msgstr "申請資産" + #: tickets/models/ticket/command_confirm.py:9 msgid "Run user" msgstr "ユーザーの実行" @@ -5856,11 +6120,11 @@ msgstr "実行コマンド" msgid "Command filter acl" msgstr "コマンドフィルター" -#: tickets/models/ticket/general.py:75 +#: tickets/models/ticket/general.py:76 msgid "Ticket step" msgstr "チケットステップ" -#: tickets/models/ticket/general.py:93 +#: tickets/models/ticket/general.py:94 msgid "Ticket assignee" msgstr "割り当てられたチケット" @@ -5888,7 +6152,7 @@ msgstr "製造オーダスナップショット" msgid "Please try again" msgstr "もう一度お試しください" -#: tickets/models/ticket/general.py:425 +#: tickets/models/ticket/general.py:458 msgid "Super ticket" msgstr "スーパーチケット" @@ -5933,19 +6197,19 @@ msgstr "チケットが処理されました。プロセッサー- {}" msgid "Ticket has processed - {} ({})" msgstr "チケットが処理済み- {} ({})" -#: tickets/serializers/flow.py:21 +#: tickets/serializers/flow.py:20 msgid "Assignees display" msgstr "受付者名" -#: tickets/serializers/flow.py:47 +#: tickets/serializers/flow.py:46 msgid "Please select the Assignees" msgstr "受付をお選びください" -#: tickets/serializers/flow.py:75 +#: tickets/serializers/flow.py:74 msgid "The current organization type already exists" msgstr "現在の組織タイプは既に存在します。" -#: tickets/serializers/super_ticket.py:11 +#: tickets/serializers/super_ticket.py:15 msgid "Processor" msgstr "プロセッサ" @@ -5953,6 +6217,14 @@ msgstr "プロセッサ" msgid "Support fuzzy search, and display up to 10 items" msgstr "ファジー検索をサポートし、最大10項目を表示します。" +#: tickets/serializers/ticket/apply_asset.py:22 +msgid "Apply assets" +msgstr "申請資産" + +#: tickets/serializers/ticket/apply_asset.py:26 +msgid "Apply nodes" +msgstr "ノードの適用" + #: tickets/serializers/ticket/apply_asset.py:28 msgid "Apply actions" msgstr "申請アクション" @@ -5970,7 +6242,7 @@ msgstr "有効期限は開始日より大きくする必要があります" msgid "Permission named `{}` already exists" msgstr "'{}'という名前の権限は既に存在します" -#: tickets/serializers/ticket/ticket.py:95 +#: tickets/serializers/ticket/ticket.py:88 msgid "The ticket flow `{}` does not exist" msgstr "チケットフロー '{}'が存在しない" @@ -6141,10 +6413,6 @@ msgstr "公開キー" msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:631 -msgid "Local" -msgstr "ローカル" - #: users/models/user.py:687 users/serializers/user.py:160 msgid "Is service account" msgstr "サービスアカウントです" @@ -6178,10 +6446,6 @@ msgstr "秘密キー" msgid "Is first login" msgstr "最初のログインです" -#: users/models/user.py:727 -msgid "Source" -msgstr "ソース" - #: users/models/user.py:731 msgid "Date password last updated" msgstr "最終更新日パスワード" @@ -7118,26 +7382,107 @@ msgstr "ライセンスのインポートに成功" msgid "License is invalid" msgstr "ライセンスが無効です" -#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:127 +#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:135 msgid "License" msgstr "ライセンス" -#: xpack/plugins/license/models.py:71 +#: xpack/plugins/license/models.py:79 msgid "Standard edition" msgstr "標準版" -#: xpack/plugins/license/models.py:73 +#: xpack/plugins/license/models.py:81 msgid "Enterprise edition" msgstr "エンタープライズ版" -#: xpack/plugins/license/models.py:75 +#: xpack/plugins/license/models.py:83 msgid "Ultimate edition" msgstr "究極のエディション" -#: xpack/plugins/license/models.py:77 +#: xpack/plugins/license/models.py:85 msgid "Community edition" msgstr "コミュニティ版" +#, fuzzy +#~| msgid "Dynamic user" +#~ msgid "Dynamic username" +#~ msgstr "動的コード" + +#, fuzzy +#~| msgid "Permission named `{}` already exists" +#~ msgid "Username already exists" +#~ msgstr "'{}'という名前の権限は既に存在します" + +#, fuzzy +#~| msgid "Permission named `{}` already exists" +#~ msgid "Dynamic username already exists" +#~ msgstr "'{}'という名前の権限は既に存在します" + +#~ msgid "Commands" +#~ msgstr "コマンド#コマンド#" + +#, fuzzy +#~ msgid "Change password enabled" +#~ msgstr "パスワードの変更" + +#, fuzzy +#~ msgid "Change password method" +#~ msgstr "パスワードの変更" + +#~ msgid "{} used account[{}], login method[{}] login the asset." +#~ msgstr "" +#~ "{} トムはアカウント[{}]、ログイン方法[{}]を使ってこの資産を登録しました" + +#~ msgid "User {} has executed change auth plan for this account.({})" +#~ msgstr "ユーザー {} はこのアカウントのために改密計画を実行しました。({})" + +#~ msgid "" +#~ "The range of ports that Magnus listens on is modified in the " +#~ "configuration file" +#~ msgstr "Magnus がリッスンするポート範囲を構成ファイルで変更してください" + +#~ msgid "Magnus Listen port range" +#~ msgstr "Magnus がリッスンするポート範囲" + +#, fuzzy +#~ msgid "Published" +#~ msgstr "公開キー" + +#, fuzzy +#~ msgid "Unpublished" +#~ msgstr "終了" + +#, fuzzy +#~ msgid "Not match" +#~ msgstr "ユーザーにマッチしなかった" + +#~ msgid "Magnus listen db port" +#~ msgstr "Magnus がリッスンするデータベース ポート" + +#~ msgid "Update task content: {}" +#~ msgstr "タスク内容の更新: {}" + +#~ msgid "Material" +#~ msgstr "マテリアル" + +#~ msgid "Execute batch command" +#~ msgstr "バッチ実行コマンド" + +#, fuzzy +#~ msgid "Create account" +#~ msgstr "アカウントを集める" + +#~ msgid "Present" +#~ msgstr "プレゼント" + +#~ msgid "Date last login" +#~ msgstr "最終ログイン日" + +#~ msgid "IP last login" +#~ msgstr "IP最終ログイン" + +#~ msgid "GatherUser" +#~ msgstr "収集ユーザー" + #~ msgid "Welcome back, please enter username and password to login" #~ msgstr "" #~ "おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" @@ -7405,9 +7750,6 @@ msgstr "コミュニティ版" #~ msgid "Default Cluster" #~ msgstr "デフォルトクラスター" -#~ msgid "Test gateway" -#~ msgstr "テストゲートウェイ" - #~ msgid "User groups" #~ msgstr "ユーザーグループ" @@ -7480,12 +7822,6 @@ msgstr "コミュニティ版" #~ msgid "Only system users with automatic login are allowed" #~ msgstr "自動ログインを持つシステムユーザーのみが許可されます" -#~ msgid "System user name" -#~ msgstr "システムユーザー名" - -#~ msgid "Asset hostname" -#~ msgstr "資産ホスト名" - #~ msgid "The asset {} system platform {} does not support run Ansible tasks" #~ msgstr "" #~ "資産 {} システムプラットフォーム {} はAnsibleタスクの実行をサポートしてい" @@ -7589,9 +7925,6 @@ msgstr "コミュニティ版" #~ msgid "Callback" #~ msgstr "コールバック" -#~ msgid "Can view task monitor" -#~ msgstr "タスクモニターを表示できます" - #~ msgid "Tasks" #~ msgstr "タスク" @@ -7631,7 +7964,7 @@ msgstr "コミュニティ版" #~ msgid "Clean task history period" #~ msgstr "クリーンなタスク履歴期間" -#~ msgid "The administrator is modifying permissions. Please wait" +#~ msgid "The administrator is modifyidng permissions. Please wait" #~ msgstr "管理者は権限を変更しています。お待ちください" #~ msgid "The authorization cannot be revoked for the time being" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index f75598dd1..230afb603 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c73f2cbae215e4169dd5d71cb12c434caf465f2d81e37bb90aa2305cebb3ab39 -size 105850 +oid sha256:4af8f2ead4a9d5aaf943efea76305d8cad1ff0692758d21a93937601c6f150fd +size 105736 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index c7ad08aa0..219de87f3 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-27 12:11+0800\n" +"POT-Creation-Date: 2023-01-16 14:24+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,6 +17,653 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.4.3\n" +#: accounts/api/automations/base.py:79 +msgid "The parameter 'action' must be [{}]" +msgstr "参数 'action' 必须是 [{}]" + +#: accounts/const/account.py:6 +#: accounts/serializers/automations/change_secret.py:33 +#: assets/models/_user.py:35 audits/signal_handlers.py:51 +#: authentication/confirm/password.py:9 authentication/forms.py:32 +#: authentication/templates/authentication/login.html:288 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 +#: users/forms/profile.py:22 users/serializers/user.py:97 +#: users/templates/users/_msg_user_created.html:13 +#: users/templates/users/user_password_verify.html:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 +msgid "Password" +msgstr "密码" + +#: accounts/const/account.py:7 +#: accounts/serializers/automations/change_secret.py:34 +msgid "SSH key" +msgstr "SSH 密钥" + +#: accounts/const/account.py:8 authentication/models/access_key.py:33 +msgid "Access key" +msgstr "Access key" + +#: accounts/const/account.py:9 assets/models/_user.py:38 +#: authentication/models/sso_token.py:14 +msgid "Token" +msgstr "Token" + +#: accounts/const/account.py:13 common/db/fields.py:235 +#: settings/serializers/terminal.py:14 +msgid "All" +msgstr "全部" + +#: accounts/const/account.py:14 +msgid "Manual input" +msgstr "手动输入" + +#: accounts/const/account.py:15 +msgid "Dynamic user" +msgstr "同名账号" + +#: accounts/const/account.py:19 users/models/user.py:631 +msgid "Local" +msgstr "数据库" + +#: accounts/const/account.py:20 +#, fuzzy +#| msgid "Collect" +msgid "Collected" +msgstr "收集" + +#: accounts/const/automation.py:22 rbac/tree.py:51 +#, fuzzy +#| msgid "Push asset account" +msgid "Push account" +msgstr "账号推送" + +#: accounts/const/automation.py:23 +msgid "Change secret" +msgstr "更改密码" + +#: accounts/const/automation.py:24 +msgid "Verify account" +msgstr "验证账号" + +#: accounts/const/automation.py:25 +msgid "Gather accounts" +msgstr "收集账号" + +#: accounts/const/automation.py:43 +msgid "Specific password" +msgstr "指定" + +#: accounts/const/automation.py:44 +msgid "Random" +msgstr "" + +#: accounts/const/automation.py:48 ops/const.py:13 +msgid "Append SSH KEY" +msgstr "追加" + +#: accounts/const/automation.py:49 ops/const.py:14 +msgid "Empty and append SSH KEY" +msgstr "清空所有并添加" + +#: accounts/const/automation.py:50 ops/const.py:15 +msgid "Replace (The key generated by JumpServer) " +msgstr "替换 (仅替换由 JumpServer 生成的密钥)" + +#: accounts/const/automation.py:55 +msgid "On asset create" +msgstr "资产创建时" + +#: accounts/const/automation.py:58 +#, fuzzy +#| msgid "On perm change" +msgid "On perm add user" +msgstr "授权变更时" + +#: accounts/const/automation.py:60 +msgid "On perm add user group" +msgstr "" + +#: accounts/const/automation.py:62 +#, fuzzy +#| msgid "permed assets" +msgid "On perm add asset" +msgstr "授权的资产" + +#: accounts/const/automation.py:64 +#, fuzzy +#| msgid "On perm change" +msgid "On perm add node" +msgstr "授权变更时" + +#: accounts/const/automation.py:66 +#, fuzzy +#| msgid "Permed account" +msgid "On perm add account" +msgstr "授权账号" + +#: accounts/const/automation.py:68 +#, fuzzy +#| msgid "Add asset to node" +msgid "On asset join node" +msgstr "添加资产到节点" + +#: accounts/const/automation.py:70 +#, fuzzy +#| msgid "User group" +msgid "On user join group" +msgstr "用户组" + +#: accounts/const/automation.py:78 +msgid "On perm change" +msgstr "授权变更时" + +#: accounts/const/automation.py:85 +#, fuzzy +#| msgid "Perm ungroup node" +msgid "Inherit from group or node" +msgstr "显示未分组节点" + +#: accounts/const/automation.py:93 +msgid "Create and push" +msgstr "创建并推送到资产" + +#: accounts/const/automation.py:94 +msgid "Only create" +msgstr "仅创建到资产" + +#: accounts/models/account.py:47 accounts/serializers/account/account.py:77 +#: accounts/serializers/automations/change_secret.py:107 +#: accounts/serializers/automations/change_secret.py:127 acls/models/base.py:96 +#: acls/serializers/base.py:56 assets/models/asset/common.py:96 +#: assets/models/asset/common.py:281 assets/models/cmd_filter.py:36 +#: assets/serializers/domain.py:19 assets/serializers/label.py:27 +#: audits/models.py:34 authentication/models/connection_token.py:32 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:27 +#: terminal/backends/command/models.py:21 +#: terminal/backends/command/serializers.py:14 +#: terminal/models/session/session.py:31 terminal/notifications.py:93 +#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:220 +msgid "Asset" +msgstr "资产" + +#: accounts/models/account.py:51 accounts/serializers/account/account.py:81 +#: authentication/serializers/connect_token_secret.py:49 +msgid "Su from" +msgstr "切换自" + +#: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 +#: terminal/models/applet/applet.py:25 +msgid "Version" +msgstr "版本" + +#: accounts/models/account.py:55 accounts/serializers/account/account.py:78 +#: users/models/user.py:727 +msgid "Source" +msgstr "来源" + +#: accounts/models/account.py:58 +#: accounts/serializers/automations/change_secret.py:108 +#: accounts/serializers/automations/change_secret.py:128 acls/models/base.py:98 +#: acls/serializers/base.py:57 assets/serializers/asset/common.py:125 +#: assets/serializers/gateway.py:30 audits/models.py:35 ops/models/base.py:18 +#: terminal/backends/command/models.py:22 terminal/models/session/session.py:33 +#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 +msgid "Account" +msgstr "账号" + +#: accounts/models/account.py:64 +msgid "Can view asset account secret" +msgstr "可以查看资产账号密码" + +#: accounts/models/account.py:65 +msgid "Can change asset account secret" +msgstr "可以更改资产账号密码" + +#: accounts/models/account.py:66 +msgid "Can view asset history account" +msgstr "可以查看资产历史账号" + +#: accounts/models/account.py:67 +msgid "Can view asset history account secret" +msgstr "可以查看资产历史账号密码" + +#: accounts/models/account.py:104 accounts/serializers/account/account.py:16 +msgid "Account template" +msgstr "账号模版" + +#: accounts/models/account.py:109 +msgid "Can view asset account template secret" +msgstr "可以查看资产账号密码" + +#: accounts/models/account.py:110 +msgid "Can change asset account template secret" +msgstr "可以更改账号模版密码" + +#: accounts/models/automations/backup_account.py:25 +#: accounts/models/automations/change_secret.py:47 +#: accounts/serializers/account/backup.py:29 +#: accounts/serializers/automations/change_secret.py:56 +msgid "Recipient" +msgstr "收件人" + +#: accounts/models/automations/backup_account.py:34 +#: accounts/models/automations/backup_account.py:96 +msgid "Account backup plan" +msgstr "账号备份计划" + +#: accounts/models/automations/backup_account.py:77 +#: assets/models/automations/base.py:102 audits/models.py:41 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:107 +#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:108 +#: terminal/models/session/session.py:43 +#: tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:19 +msgid "Date start" +msgstr "开始日期" + +#: accounts/models/automations/backup_account.py:80 +#: authentication/templates/authentication/_msg_oauth_bind.html:11 +#: notifications/notifications.py:186 +msgid "Time" +msgstr "时间" + +#: accounts/models/automations/backup_account.py:84 +msgid "Account backup snapshot" +msgstr "账号备份快照" + +#: accounts/models/automations/backup_account.py:88 +#: accounts/serializers/automations/base.py:42 +#: assets/models/automations/base.py:109 +#: assets/serializers/automations/base.py:40 +msgid "Trigger mode" +msgstr "触发模式" + +#: accounts/models/automations/backup_account.py:91 audits/models.py:130 +#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "原因" + +#: accounts/models/automations/backup_account.py:93 +#: accounts/serializers/automations/change_secret.py:106 +#: accounts/serializers/automations/change_secret.py:129 +#: ops/serializers/job.py:54 terminal/serializers/session.py:49 +msgid "Is success" +msgstr "是否成功" + +#: accounts/models/automations/backup_account.py:101 +msgid "Account backup execution" +msgstr "账号备份执行" + +#: accounts/models/automations/base.py:15 +#, fuzzy +#| msgid "Automation task" +msgid "Account automation task" +msgstr "自动化任务" + +#: accounts/models/automations/base.py:25 +#, fuzzy +#| msgid "Automation task execution" +msgid "Automation execution" +msgstr "自动化任务执行历史" + +#: accounts/models/automations/base.py:26 +#, fuzzy +#| msgid "Automation task execution" +msgid "Automation executions" +msgstr "自动化任务执行历史" + +#: accounts/models/automations/base.py:28 +msgid "Can view change secret execution" +msgstr "查看改密执行" + +#: accounts/models/automations/base.py:29 +msgid "Can add change secret execution" +msgstr "创建改密执行" + +#: accounts/models/automations/base.py:31 +msgid "Can view gather accounts execution" +msgstr "查看收集账号执行" + +#: accounts/models/automations/base.py:32 +msgid "Can add gather accounts execution" +msgstr "创建收集账号执行" + +#: accounts/models/automations/base.py:34 +#, fuzzy +#| msgid "Can view gather accounts execution" +msgid "Can view push account execution" +msgstr "查看收集账号执行" + +#: accounts/models/automations/base.py:35 +#, fuzzy +#| msgid "Can add gather accounts execution" +msgid "Can add push account execution" +msgstr "创建收集账号执行" + +#: accounts/models/automations/change_secret.py:17 accounts/models/base.py:36 +#: accounts/serializers/account/account.py:114 +#: accounts/serializers/account/base.py:16 +#: accounts/serializers/automations/change_secret.py:46 +#: authentication/serializers/connect_token_secret.py:40 +#: authentication/serializers/connect_token_secret.py:50 +msgid "Secret type" +msgstr "密文类型" + +#: accounts/models/automations/change_secret.py:21 +#: accounts/serializers/automations/change_secret.py:40 +msgid "Secret strategy" +msgstr "密文策略" + +#: accounts/models/automations/change_secret.py:23 +#: accounts/models/automations/change_secret.py:72 accounts/models/base.py:38 +#: accounts/serializers/account/base.py:19 +#: authentication/models/temp_token.py:10 +#: authentication/templates/authentication/_access_key_modal.html:31 +#: settings/serializers/auth/radius.py:19 +msgid "Secret" +msgstr "密钥" + +#: accounts/models/automations/change_secret.py:24 +msgid "Password rules" +msgstr "密码规则" + +#: accounts/models/automations/change_secret.py:27 +msgid "SSH key change strategy" +msgstr "SSH 密钥推送方式" + +#: accounts/models/automations/change_secret.py:54 +msgid "Change secret automation" +msgstr "自动化改密" + +#: accounts/models/automations/change_secret.py:71 +msgid "Old secret" +msgstr "原密码" + +#: accounts/models/automations/change_secret.py:73 +msgid "Date started" +msgstr "开始日期" + +#: accounts/models/automations/change_secret.py:74 +#: assets/models/automations/base.py:103 ops/models/base.py:56 +#: ops/models/celery.py:64 ops/models/job.py:108 +#: terminal/models/applet/host.py:109 +msgid "Date finished" +msgstr "结束日期" + +#: accounts/models/automations/change_secret.py:76 common/const/choices.py:20 +msgid "Error" +msgstr "错误" + +#: accounts/models/automations/change_secret.py:80 +msgid "Change secret record" +msgstr "改密记录" + +#: accounts/models/automations/gather_account.py:15 +#: accounts/tasks/gather_accounts.py:28 +msgid "Gather asset accounts" +msgstr "收集账号" + +#: accounts/models/automations/push_account.py:13 +#, fuzzy +#| msgid "Trigger" +msgid "Triggers" +msgstr "触发方式" + +#: accounts/models/automations/push_account.py:14 accounts/models/base.py:34 +#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: assets/models/_user.py:34 audits/models.py:115 authentication/forms.py:25 +#: authentication/forms.py:27 authentication/models/temp_token.py:9 +#: authentication/templates/authentication/_msg_different_city.html:9 +#: authentication/templates/authentication/_msg_oauth_bind.html:9 +#: users/forms/profile.py:32 users/forms/profile.py:112 +#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 +msgid "Username" +msgstr "用户名" + +#: accounts/models/automations/push_account.py:15 acls/models/base.py:77 +#: acls/serializers/base.py:81 assets/models/cmd_filter.py:81 +#: audits/models.py:51 audits/serializers.py:75 +#: authentication/serializers/connect_token_secret.py:108 +#: authentication/templates/authentication/_access_key_modal.html:34 +msgid "Action" +msgstr "动作" + +#: accounts/models/automations/push_account.py:41 +msgid "Push asset account" +msgstr "账号推送" + +#: accounts/models/automations/verify_account.py:15 +msgid "Verify asset account" +msgstr "账号验证" + +#: accounts/models/base.py:33 acls/models/base.py:71 +#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: applications/models.py:9 assets/models/_user.py:33 +#: assets/models/asset/common.py:94 assets/models/asset/common.py:106 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 +#: assets/models/group.py:20 assets/models/label.py:18 +#: assets/models/platform.py:20 assets/models/platform.py:74 +#: assets/serializers/asset/common.py:143 assets/serializers/platform.py:128 +#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 +#: ops/models/adhoc.py:22 ops/models/celery.py:15 ops/models/celery.py:57 +#: ops/models/job.py:25 ops/models/playbook.py:14 orgs/models.py:69 +#: perms/models/asset_permission.py:56 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 +#: terminal/models/applet/applet.py:23 terminal/models/component/endpoint.py:12 +#: terminal/models/component/endpoint.py:90 +#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 +#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: users/models/group.py:13 users/models/user.py:675 +#: xpack/plugins/cloud/models.py:28 +msgid "Name" +msgstr "名称" + +#: accounts/models/base.py:39 +msgid "Privileged" +msgstr "特权账号" + +#: accounts/models/base.py:40 assets/models/asset/common.py:113 +#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 +#: assets/models/label.py:22 +#: authentication/serializers/connect_token_secret.py:106 +#: terminal/models/applet/applet.py:28 users/serializers/user.py:158 +msgid "Is active" +msgstr "激活" + +#: accounts/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "账号备份任务结果通知" + +#: accounts/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "{} - 账号备份任务已完成, 详情见附件" + +#: accounts/notifications.py:20 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" +"置加密密码" + +#: accounts/notifications.py:31 +msgid "Notification of implementation result of encryption change plan" +msgstr "改密计划任务结果通知" + +#: accounts/notifications.py:41 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} - 改密任务已完成, 详情见附件" + +#: accounts/notifications.py:42 +msgid "" +"{} - The encryption change task has been completed: the encryption password " +"has not been set - please go to personal information -> file encryption " +"password to set the encryption password" +msgstr "" +"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" +"密密码" + +#: accounts/serializers/account/account.py:19 +#: assets/serializers/asset/common.py:52 +msgid "Push now" +msgstr "立即推送" + +#: accounts/serializers/account/account.py:21 +#: accounts/serializers/account/base.py:64 +msgid "Has secret" +msgstr "已托管密码" + +#: accounts/serializers/account/account.py:28 +#: assets/serializers/asset/common.py:79 +msgid "Account template not found" +msgstr "账号模版未找到" + +#: accounts/serializers/account/account.py:73 +msgid "Asset not found" +msgstr "资产不存在" + +#: accounts/serializers/account/backup.py:27 +#: accounts/serializers/automations/base.py:35 +#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 +#: settings/serializers/auth/ldap.py:66 +msgid "Periodic perform" +msgstr "定时执行" + +#: accounts/serializers/account/backup.py:28 +#: accounts/serializers/automations/gather_accounts.py:23 +msgid "Executed amount" +msgstr "执行次数" + +#: accounts/serializers/account/backup.py:30 +#: accounts/serializers/automations/change_secret.py:57 +msgid "Currently only mail sending is supported" +msgstr "当前只支持邮件发送" + +#: accounts/serializers/account/base.py:24 +msgid "Key password" +msgstr "密钥密码" + +#: accounts/serializers/account/base.py:80 +msgid "Specific" +msgstr "指定的" + +#: accounts/serializers/automations/base.py:21 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:35 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "资产" + +#: accounts/serializers/automations/base.py:22 +#: assets/models/asset/common.py:112 assets/models/automations/base.py:18 +#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:67 +msgid "Nodes" +msgstr "节点" + +#: accounts/serializers/automations/base.py:40 +#: assets/models/automations/base.py:105 +#: assets/serializers/automations/base.py:39 +msgid "Automation snapshot" +msgstr "工单快照" + +#: accounts/serializers/automations/base.py:41 acls/models/command_acl.py:24 +#: acls/serializers/command_acl.py:18 applications/models.py:14 +#: assets/models/_user.py:46 assets/models/automations/base.py:20 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:76 +#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:86 +#: audits/serializers.py:47 +#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:33 +#: perms/serializers/user_permission.py:26 terminal/models/applet/applet.py:27 +#: terminal/models/component/storage.py:57 +#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:28 +#: terminal/serializers/session.py:26 terminal/serializers/storage.py:181 +#: tickets/models/comment.py:26 tickets/models/flow.py:56 +#: tickets/models/ticket/apply_application.py:16 +#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53 +#: tickets/serializers/ticket/ticket.py:19 +msgid "Type" +msgstr "类型" + +#: accounts/serializers/automations/change_secret.py:43 +msgid "SSH Key strategy" +msgstr "SSH 密钥更改方式" + +#: accounts/serializers/automations/change_secret.py:76 +msgid "* Please enter the correct password length" +msgstr "* 请输入正确的密码长度" + +#: accounts/serializers/automations/change_secret.py:80 +msgid "* Password length range 6-30 bits" +msgstr "* 密码长度范围 6-30 位" + +#: accounts/serializers/automations/change_secret.py:110 +#: assets/models/automations/base.py:114 +msgid "Automation task execution" +msgstr "自动化任务执行历史" + +#: accounts/serializers/automations/change_secret.py:150 audits/const.py:45 +#: audits/models.py:40 common/const/choices.py:18 ops/const.py:51 +#: ops/serializers/celery.py:39 terminal/const.py:59 +#: terminal/models/session/sharing.py:103 tickets/views/approve.py:114 +msgid "Success" +msgstr "成功" + +#: accounts/serializers/automations/change_secret.py:151 +#: assets/const/automation.py:8 audits/const.py:46 common/const/choices.py:19 +#: ops/const.py:53 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失败" + +#: accounts/tasks/automation.py:11 +#, fuzzy +#| msgid "Execute automation" +msgid "Account execute automation" +msgstr "执行自动化任务" + +#: accounts/tasks/backup_account.py:13 +msgid "Execute account backup plan" +msgstr "执行账号备份计划" + +#: accounts/tasks/gather_accounts.py:31 +msgid "Gather assets accounts" +msgstr "收集资产上的账号" + +#: accounts/tasks/push_account.py:30 accounts/tasks/push_account.py:36 +msgid "Push accounts to assets" +msgstr "推送账号到资产" + +#: accounts/tasks/verify_account.py:30 +msgid "Verify asset account availability" +msgstr "" + +#: accounts/tasks/verify_account.py:36 +msgid "Verify accounts connectivity" +msgstr "测试账号可连接性" + +#: accounts/utils.py:42 +msgid "Password can not contains `{{` " +msgstr "密码不能包含 `{{` 字符" + +#: accounts/utils.py:45 +msgid "Password can not contains `'` " +msgstr "密码不能包含 `'` 字符" + +#: accounts/utils.py:47 +msgid "Password can not contains `\"` " +msgstr "密码不能包含 `\"` 字符" + +#: accounts/utils.py:53 +msgid "private key invalid or passphrase error" +msgstr "密钥不合法或密钥密码错误" + #: acls/apps.py:7 msgid "Acls" msgstr "访问控制" @@ -34,67 +681,39 @@ msgstr "接受" msgid "Review" msgstr "审批" -#: acls/models/base.py:71 acls/models/command_acl.py:21 -#: acls/serializers/base.py:34 applications/models.py:9 -#: assets/models/_user.py:33 assets/models/asset/common.py:92 -#: assets/models/asset/common.py:101 assets/models/base.py:64 -#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:20 assets/models/platform.py:71 -#: assets/serializers/asset/common.py:87 assets/serializers/platform.py:121 -#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 -#: ops/models/adhoc.py:23 ops/models/celery.py:15 ops/models/job.py:24 -#: ops/models/playbook.py:14 orgs/models.py:67 -#: perms/models/asset_permission.py:55 rbac/models/role.py:29 -#: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/applet/applet.py:22 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:86 -#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 -#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/user.py:675 -#: xpack/plugins/cloud/models.py:28 -msgid "Name" -msgstr "名称" - #: acls/models/base.py:73 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:89 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:93 msgid "Priority" msgstr "优先级" #: acls/models/base.py:74 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:90 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:94 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" -#: acls/models/base.py:77 acls/serializers/base.py:63 -#: assets/models/cmd_filter.py:81 audits/models.py:51 audits/serializers.py:73 -#: authentication/serializers/connect_token_secret.py:108 -#: authentication/templates/authentication/_access_key_modal.html:34 -msgid "Action" -msgstr "动作" - -#: acls/models/base.py:78 acls/serializers/base.py:59 +#: acls/models/base.py:78 acls/serializers/base.py:75 #: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:81 msgid "Reviewers" msgstr "审批人" #: acls/models/base.py:79 authentication/models/access_key.py:17 +#: authentication/models/connection_token.py:47 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:75 terminal/models/session/sharing.py:27 +#: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 msgid "Active" msgstr "激活中" -#: acls/models/base.py:91 acls/models/login_acl.py:13 +#: acls/models/base.py:94 acls/models/login_acl.py:13 #: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:30 -#: audits/models.py:49 audits/models.py:93 +#: audits/models.py:49 audits/models.py:99 #: authentication/models/connection_token.py:28 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 -#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:57 -#: perms/serializers/permission.py:23 rbac/builtin.py:120 +#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 +#: perms/serializers/permission.py:23 rbac/builtin.py:118 #: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 #: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 @@ -104,35 +723,8 @@ msgstr "激活中" msgid "User" msgstr "用户" -#: acls/models/base.py:93 acls/serializers/base.py:56 -#: assets/models/account.py:50 assets/models/asset/common.py:94 -#: assets/models/asset/common.py:221 assets/models/cmd_filter.py:36 -#: assets/models/gathered_user.py:12 assets/serializers/account/account.py:76 -#: assets/serializers/automations/change_secret.py:100 -#: assets/serializers/automations/change_secret.py:122 -#: assets/serializers/domain.py:19 assets/serializers/gathered_user.py:11 -#: assets/serializers/label.py:27 audits/models.py:34 -#: authentication/models/connection_token.py:32 -#: perms/models/asset_permission.py:63 perms/serializers/permission.py:27 -#: terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 -#: terminal/models/session/session.py:31 terminal/notifications.py:93 -#: xpack/plugins/cloud/models.py:220 -msgid "Asset" -msgstr "资产" - -#: acls/models/base.py:95 acls/serializers/base.py:57 -#: assets/models/account.py:60 -#: assets/serializers/automations/change_secret.py:101 -#: assets/serializers/automations/change_secret.py:123 audits/models.py:35 -#: ops/models/base.py:18 terminal/backends/command/models.py:22 -#: terminal/models/session/session.py:33 -#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 -msgid "Account" -msgstr "账号" - #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: terminal/backends/command/serializers.py:15 +#: ops/serializers/job.py:53 terminal/backends/command/serializers.py:15 #: terminal/models/session/session.py:41 terminal/serializers/session.py:19 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -143,23 +735,6 @@ msgstr "命令" msgid "Regex" msgstr "正则表达式" -#: acls/models/command_acl.py:24 acls/serializers/command_acl.py:19 -#: applications/models.py:14 assets/models/_user.py:46 -#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 -#: assets/models/platform.py:73 assets/serializers/asset/common.py:63 -#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:86 -#: audits/serializers.py:45 -#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:32 -#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:26 -#: terminal/models/component/storage.py:57 -#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:33 -#: terminal/serializers/session.py:25 tickets/models/comment.py:26 -#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 -#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:54 -#: tickets/serializers/ticket/ticket.py:19 -msgid "Type" -msgstr "类型" - #: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 #: settings/serializers/basic.py:10 xpack/plugins/license/models.py:29 msgid "Content" @@ -173,7 +748,8 @@ msgstr "每行一个命令" msgid "Ignore case" msgstr "忽略大小写" -#: acls/models/command_acl.py:33 acls/serializers/command_acl.py:29 +#: acls/models/command_acl.py:33 acls/models/command_acl.py:96 +#: acls/serializers/command_acl.py:28 #: authentication/serializers/connect_token_secret.py:78 msgid "Command group" msgstr "命令组" @@ -182,10 +758,6 @@ msgstr "命令组" msgid "The generated regular expression is incorrect: {}" msgstr "生成的正则表达式有误" -#: acls/models/command_acl.py:96 -msgid "Commands" -msgstr "命令" - #: acls/models/command_acl.py:100 msgid "Command acl" msgstr "命令过滤" @@ -194,7 +766,7 @@ msgstr "命令过滤" msgid "Command confirm" msgstr "命令复核" -#: acls/models/login_acl.py:16 +#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:29 msgid "Rule" msgstr "规则" @@ -218,19 +790,6 @@ msgstr "登录资产复核" msgid "Format for comma-delimited string, with * indicating a match all. " msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " -#: acls/serializers/base.py:18 acls/serializers/base.py:49 -#: assets/models/_user.py:34 assets/models/base.py:65 -#: assets/models/gathered_user.py:13 audits/models.py:109 -#: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models/temp_token.py:9 -#: authentication/templates/authentication/_msg_different_city.html:9 -#: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: users/forms/profile.py:32 users/forms/profile.py:112 -#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 -msgid "Username" -msgstr "用户名" - #: acls/serializers/base.py:25 msgid "" "Format for comma-delimited string, with * indicating a match all. Such as: " @@ -244,14 +803,50 @@ msgstr "" msgid "IP/Host" msgstr "IP/主机" -#: acls/serializers/base.py:90 tickets/serializers/ticket/ticket.py:78 +#: acls/serializers/base.py:60 +#, fuzzy +#| msgid "System user name" +msgid "User (username)" +msgstr "系统用户名称" + +#: acls/serializers/base.py:64 +#, fuzzy +#| msgid "Asset hostname" +msgid "Asset (name)" +msgstr "资产主机名" + +#: acls/serializers/base.py:68 +#, fuzzy +#| msgid "Address" +msgid "Asset (address)" +msgstr "地址" + +#: acls/serializers/base.py:72 +#, fuzzy +#| msgid "Account name" +msgid "Account (username)" +msgstr "账号名称" + +#: acls/serializers/base.py:78 acls/serializers/login_acl.py:27 +#, fuzzy +#| msgid "Reviewers" +msgid "Reviewers amount" +msgstr "审批人" + +#: acls/serializers/base.py:109 tickets/serializers/ticket/ticket.py:76 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/base.py:96 +#: acls/serializers/base.py:115 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" +#: acls/serializers/command_acl.py:31 +#, fuzzy +#| msgid "Command amount" +msgid "Command group amount" +msgstr "命令数量" + #: acls/serializers/rules/rules.py:20 #: xpack/plugins/cloud/serializers/task.py:22 msgid "IP address invalid: `{}`" @@ -266,11 +861,11 @@ msgstr "" "格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" -#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:102 +#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:107 #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61 msgid "IP" msgstr "IP" @@ -283,9 +878,9 @@ msgid "Applications" msgstr "应用管理" #: applications/models.py:11 assets/models/label.py:21 -#: assets/models/platform.py:72 assets/serializers/asset/common.py:62 +#: assets/models/platform.py:75 assets/serializers/asset/common.py:121 #: assets/serializers/cagegory.py:8 assets/serializers/platform.py:87 -#: assets/serializers/platform.py:122 perms/serializers/user_permission.py:23 +#: assets/serializers/platform.py:129 perms/serializers/user_permission.py:25 #: settings/models.py:35 tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "类别" @@ -304,7 +899,7 @@ msgid "Can match application" msgstr "匹配应用" #: applications/serializers/attrs/application_type/clickhouse.py:11 -#: assets/models/asset/common.py:93 assets/models/platform.py:21 +#: assets/models/asset/common.py:95 assets/models/platform.py:21 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" @@ -316,11 +911,7 @@ msgid "" "different ports" msgstr "默认端口为9000, HTTP接口和本机接口使用不同的端口" -#: assets/api/automations/base.py:77 -msgid "The parameter 'action' must be [{}]" -msgstr "参数 'action' 必须是 [{}]" - -#: assets/api/domain.py:56 +#: assets/api/domain.py:57 msgid "Number required" msgstr "需要为数字" @@ -340,103 +931,56 @@ msgstr "删除失败,节点包含资产" msgid "App assets" msgstr "资产管理" -#: assets/automations/base/manager.py:123 +#: assets/automations/base/manager.py:76 msgid "{} disabled" msgstr "{} 已禁用" -#: assets/const/account.py:6 audits/const.py:6 audits/const.py:64 +#: assets/automations/ping_gateway/manager.py:33 +#: authentication/models/connection_token.py:113 +msgid "No account" +msgstr "没有账号" + +#: assets/automations/ping_gateway/manager.py:55 +#, python-brace-format +msgid "Unable to connect to port {port} on {address}" +msgstr "无法连接到 {port} 上的端口 {address}" + +#: assets/automations/ping_gateway/manager.py:58 +#: authentication/middleware.py:76 xpack/plugins/cloud/providers/fc.py:48 +msgid "Authentication failed" +msgstr "认证失败" + +#: assets/automations/ping_gateway/manager.py:60 +#: assets/automations/ping_gateway/manager.py:86 +msgid "Connect failed" +msgstr "连接失败" + +#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:35 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" msgstr "未知" -#: assets/const/account.py:7 +#: assets/const/automation.py:7 msgid "Ok" msgstr "成功" -#: assets/const/account.py:8 -#: assets/serializers/automations/change_secret.py:118 -#: assets/serializers/automations/change_secret.py:146 audits/const.py:75 -#: common/const/choices.py:19 ops/const.py:51 xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失败" - -#: assets/const/account.py:12 assets/models/_user.py:35 -#: audits/signal_handlers.py:49 authentication/confirm/password.py:9 -#: authentication/forms.py:32 -#: authentication/templates/authentication/login.html:288 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:97 -#: users/templates/users/_msg_user_created.html:13 -#: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/cloud/serializers/account_attrs.py:28 -msgid "Password" -msgstr "密码" - -#: assets/const/account.py:13 -msgid "SSH key" -msgstr "SSH 密钥" - -#: assets/const/account.py:14 authentication/models/access_key.py:33 -msgid "Access key" -msgstr "访问密钥" - -#: assets/const/account.py:15 assets/models/_user.py:38 -#: authentication/models/sso_token.py:14 -msgid "Token" -msgstr "令牌" - -#: assets/const/automation.py:13 +#: assets/const/automation.py:12 msgid "Ping" msgstr "" +#: assets/const/automation.py:13 +#, fuzzy +#| msgid "Test gateway" +msgid "Ping gateway" +msgstr "测试网关" + #: assets/const/automation.py:14 msgid "Gather facts" msgstr "收集资产信息" -#: assets/const/automation.py:15 -msgid "Create account" -msgstr "创建账号" - -#: assets/const/automation.py:16 -msgid "Change secret" -msgstr "更改密码" - -#: assets/const/automation.py:17 -msgid "Verify account" -msgstr "验证账号" - -#: assets/const/automation.py:18 -msgid "Gather accounts" -msgstr "收集账号" - -#: assets/const/automation.py:38 assets/serializers/account/base.py:29 -msgid "Specific" -msgstr "特有的" - -#: assets/const/automation.py:39 ops/const.py:20 -msgid "All assets use the same random password" -msgstr "使用相同的随机密码" - -#: assets/const/automation.py:40 ops/const.py:21 -msgid "All assets use different random password" -msgstr "使用不同的随机密码" - -#: assets/const/automation.py:44 ops/const.py:13 -msgid "Append SSH KEY" -msgstr "追加" - -#: assets/const/automation.py:45 ops/const.py:14 -msgid "Empty and append SSH KEY" -msgstr "清空所有并添加" - -#: assets/const/automation.py:46 ops/const.py:15 -msgid "Replace (The key generated by JumpServer) " -msgstr "替换 (由 JumpServer 生成的密钥)" - #: assets/const/category.py:11 settings/serializers/auth/radius.py:16 -#: settings/serializers/auth/sms.py:67 terminal/models/applet/applet.py:129 -#: terminal/models/component/endpoint.py:13 +#: settings/serializers/auth/sms.py:67 terminal/models/component/endpoint.py:13 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "主机" @@ -445,8 +989,8 @@ msgstr "主机" msgid "Device" msgstr "网络设备" -#: assets/const/category.py:13 assets/models/asset/database.py:8 -#: assets/models/asset/database.py:34 +#: assets/const/category.py:13 assets/models/asset/database.py:9 +#: assets/models/asset/database.py:32 msgid "Database" msgstr "数据库" @@ -454,12 +998,12 @@ msgstr "数据库" msgid "Cloud service" msgstr "云服务" -#: assets/const/category.py:15 audits/const.py:62 -#: terminal/models/applet/applet.py:20 +#: assets/const/category.py:15 audits/const.py:33 +#: terminal/models/applet/applet.py:21 msgid "Web" msgstr "Web" -#: assets/const/device.py:7 terminal/models/applet/applet.py:19 +#: assets/const/device.py:7 terminal/models/applet/applet.py:20 #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -476,7 +1020,7 @@ msgstr "路由器" msgid "Firewall" msgstr "防火墙" -#: assets/const/types.py:181 +#: assets/const/types.py:200 msgid "All types" msgstr "所有类型" @@ -510,33 +1054,33 @@ msgstr "SSH公钥" #: assets/models/_user.py:40 assets/models/cmd_filter.py:40 #: assets/models/cmd_filter.py:88 assets/models/group.py:23 -#: assets/models/platform.py:76 common/db/models.py:78 ops/models/adhoc.py:29 -#: ops/models/job.py:40 ops/models/playbook.py:17 rbac/models/role.py:37 -#: settings/models.py:38 terminal/models/applet/applet.py:31 -#: terminal/models/applet/applet.py:131 terminal/models/applet/host.py:110 -#: terminal/models/component/endpoint.py:20 -#: terminal/models/component/endpoint.py:96 +#: assets/models/platform.py:79 common/db/models.py:37 ops/models/adhoc.py:28 +#: ops/models/job.py:41 ops/models/playbook.py:17 rbac/models/role.py:37 +#: settings/models.py:38 terminal/models/applet/applet.py:32 +#: terminal/models/applet/applet.py:137 terminal/models/applet/host.py:110 +#: terminal/models/component/endpoint.py:24 +#: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:45 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:297 users/models/user.py:714 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:119 msgid "Comment" msgstr "备注" -#: assets/models/_user.py:41 assets/models/automations/base.py:91 +#: assets/models/_user.py:41 assets/models/automations/base.py:101 #: assets/models/cmd_filter.py:41 assets/models/group.py:22 -#: common/db/models.py:76 ops/models/base.py:54 ops/models/job.py:105 +#: common/db/models.py:35 ops/models/base.py:54 ops/models/job.py:106 #: users/models/user.py:932 msgid "Date created" msgstr "创建日期" #: assets/models/_user.py:42 assets/models/cmd_filter.py:42 -#: common/db/models.py:77 +#: common/db/models.py:36 msgid "Date updated" msgstr "更新日期" #: assets/models/_user.py:43 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:21 -#: common/db/models.py:74 users/models/user.py:722 +#: common/db/models.py:33 users/models/user.py:722 #: users/serializers/group.py:33 msgid "Created by" msgstr "创建者" @@ -547,8 +1091,8 @@ msgstr "用户名与用户相同" #: assets/models/_user.py:48 authentication/models/connection_token.py:37 #: authentication/serializers/connect_token_secret.py:103 -#: terminal/models/applet/applet.py:29 terminal/serializers/session.py:24 -#: terminal/serializers/session.py:40 terminal/serializers/storage.py:68 +#: terminal/models/applet/applet.py:30 terminal/serializers/session.py:24 +#: terminal/serializers/session.py:45 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "协议" @@ -560,7 +1104,7 @@ msgstr "自动推送" msgid "Sudo" msgstr "Sudo" -#: assets/models/_user.py:51 ops/const.py:44 ops/models/adhoc.py:19 +#: assets/models/_user.py:51 ops/const.py:44 msgid "Shell" msgstr "Shell" @@ -596,136 +1140,75 @@ msgstr "系统用户" msgid "Can match system user" msgstr "可以匹配系统用户" -#: assets/models/account.py:44 common/db/fields.py:232 -#: settings/serializers/terminal.py:14 -msgid "All" -msgstr "全部" - -#: assets/models/account.py:45 -msgid "Manual input" -msgstr "手动输入" - -#: assets/models/account.py:46 -msgid "Dynamic user" -msgstr "同名账号" - -#: assets/models/account.py:54 assets/serializers/account/account.py:79 -#: authentication/serializers/connect_token_secret.py:48 -msgid "Su from" -msgstr "切换自" - -#: assets/models/account.py:56 settings/serializers/auth/cas.py:20 -#: terminal/models/applet/applet.py:24 -msgid "Version" -msgstr "版本" - -#: assets/models/account.py:66 -msgid "Can view asset account secret" -msgstr "可以查看资产账号密码" - -#: assets/models/account.py:67 -msgid "Can change asset account secret" -msgstr "可以更改资产账号密码" - -#: assets/models/account.py:68 -msgid "Can view asset history account" -msgstr "可以查看资产历史账号" - -#: assets/models/account.py:69 -msgid "Can view asset history account secret" -msgstr "可以查看资产历史账号密码" - -#: assets/models/account.py:106 assets/serializers/account/account.py:15 -msgid "Account template" -msgstr "账号模版" - -#: assets/models/account.py:111 -msgid "Can view asset account template secret" -msgstr "可以查看资产账号密码" - -#: assets/models/account.py:112 -msgid "Can change asset account template secret" -msgstr "可以更改账号模版密码" - -#: assets/models/asset/common.py:103 assets/models/platform.py:109 -#: assets/serializers/asset/common.py:65 +#: assets/models/asset/common.py:108 assets/models/platform.py:112 #: authentication/serializers/connect_token_secret.py:107 -#: perms/serializers/user_permission.py:21 +#: perms/serializers/user_permission.py:23 #: xpack/plugins/cloud/serializers/account_attrs.py:179 msgid "Platform" msgstr "系统平台" -#: assets/models/asset/common.py:105 assets/models/domain.py:21 -#: assets/serializers/asset/common.py:64 +#: assets/models/asset/common.py:110 assets/models/domain.py:21 #: authentication/serializers/connect_token_secret.py:125 +#: perms/serializers/user_permission.py:27 msgid "Domain" msgstr "网域" -#: assets/models/asset/common.py:107 assets/models/automations/base.py:18 -#: assets/models/cmd_filter.py:32 assets/serializers/asset/common.py:66 -#: assets/serializers/automations/base.py:21 -#: perms/models/asset_permission.py:66 -msgid "Nodes" -msgstr "节点" - -#: assets/models/asset/common.py:108 assets/models/automations/base.py:21 -#: assets/models/base.py:71 assets/models/cmd_filter.py:39 -#: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:106 -#: terminal/models/applet/applet.py:27 users/serializers/user.py:158 -msgid "Is active" -msgstr "激活" - -#: assets/models/asset/common.py:109 assets/serializers/asset/common.py:67 +#: assets/models/asset/common.py:114 msgid "Labels" msgstr "标签管理" -#: assets/models/asset/common.py:224 +#: assets/models/asset/common.py:284 msgid "Can refresh asset hardware info" msgstr "可以更新资产硬件信息" -#: assets/models/asset/common.py:225 +#: assets/models/asset/common.py:285 msgid "Can test asset connectivity" msgstr "可以测试资产连接性" -#: assets/models/asset/common.py:226 +#: assets/models/asset/common.py:286 msgid "Can push account to asset" msgstr "可以推送账号到资产" -#: assets/models/asset/common.py:227 +#: assets/models/asset/common.py:287 +#, fuzzy +#| msgid "Verify account" +msgid "Can verify account" +msgstr "验证账号" + +#: assets/models/asset/common.py:288 msgid "Can match asset" msgstr "可以匹配资产" -#: assets/models/asset/common.py:228 +#: assets/models/asset/common.py:289 msgid "Add asset to node" msgstr "添加资产到节点" -#: assets/models/asset/common.py:229 +#: assets/models/asset/common.py:290 msgid "Move asset to node" msgstr "移动资产到节点" -#: assets/models/asset/database.py:9 settings/serializers/email.py:37 +#: assets/models/asset/database.py:10 settings/serializers/email.py:37 msgid "Use SSL" msgstr "使用 SSL" -#: assets/models/asset/database.py:10 +#: assets/models/asset/database.py:11 msgid "CA cert" msgstr "CA 证书" -#: assets/models/asset/database.py:11 +#: assets/models/asset/database.py:12 msgid "Client cert" msgstr "客户端证书" -#: assets/models/asset/database.py:12 +#: assets/models/asset/database.py:13 msgid "Client key" msgstr "客户端密钥" -#: assets/models/asset/database.py:13 +#: assets/models/asset/database.py:14 msgid "Allow invalid cert" msgstr "忽略证书校验" -#: assets/models/asset/web.py:9 audits/const.py:68 -#: terminal/serializers/applet_host.py:25 +#: assets/models/asset/web.py:9 audits/const.py:39 +#: terminal/serializers/applet_host.py:27 msgid "Disabled" msgstr "禁用" @@ -755,137 +1238,32 @@ msgid "Submit selector" msgstr "确认按钮选择器" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:69 +#: assets/serializers/asset/common.py:241 perms/models/asset_permission.py:70 #: perms/serializers/permission.py:32 rbac/tree.py:36 msgid "Accounts" msgstr "账号管理" -#: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:34 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -msgid "Assets" -msgstr "资产" - -#: assets/models/automations/base.py:81 assets/models/automations/base.py:88 +#: assets/models/automations/base.py:28 assets/models/automations/base.py:98 msgid "Automation task" msgstr "自动化任务" -#: assets/models/automations/base.py:90 audits/models.py:129 -#: audits/serializers.py:46 ops/models/base.py:49 ops/models/job.py:98 -#: terminal/models/applet/applet.py:130 terminal/models/applet/host.py:107 -#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 -#: tickets/models/ticket/general.py:283 tickets/serializers/ticket/ticket.py:20 -#: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:224 +#: assets/models/automations/base.py:91 +#, fuzzy +#| msgid "Automation task" +msgid "Asset automation task" +msgstr "自动化任务" + +#: assets/models/automations/base.py:100 audits/models.py:135 +#: audits/serializers.py:48 ops/models/base.py:49 ops/models/job.py:99 +#: terminal/models/applet/applet.py:136 terminal/models/applet/host.py:107 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:17 +#: terminal/serializers/applet_host.py:90 tickets/models/ticket/general.py:283 +#: tickets/serializers/super_ticket.py:13 +#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/models.py:224 msgid "Status" msgstr "状态" -#: assets/models/automations/base.py:92 assets/models/backup.py:73 -#: audits/models.py:41 ops/models/base.py:55 ops/models/celery.py:60 -#: ops/models/job.py:106 perms/models/asset_permission.py:71 -#: terminal/models/applet/host.py:108 terminal/models/session/session.py:43 -#: tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:19 -msgid "Date start" -msgstr "开始日期" - -#: assets/models/automations/base.py:93 -#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 -#: ops/models/celery.py:61 ops/models/job.py:107 -#: terminal/models/applet/host.py:109 -msgid "Date finished" -msgstr "结束日期" - -#: assets/models/automations/base.py:95 -#: assets/serializers/automations/base.py:39 -msgid "Automation snapshot" -msgstr "工单快照" - -#: assets/models/automations/base.py:99 assets/models/backup.py:84 -#: assets/serializers/automations/base.py:41 -msgid "Trigger mode" -msgstr "触发模式" - -#: assets/models/automations/base.py:103 -#: assets/serializers/automations/change_secret.py:103 -msgid "Automation task execution" -msgstr "自动化任务执行历史" - -#: assets/models/automations/base.py:105 -msgid "Can view change secret execution" -msgstr "查看改密执行" - -#: assets/models/automations/base.py:106 -msgid "Can add change secret execution" -msgstr "创建改密执行" - -#: assets/models/automations/base.py:107 -msgid "Can view gather accounts execution" -msgstr "查看收集账号执行" - -#: assets/models/automations/base.py:108 -msgid "Can add gather accounts execution" -msgstr "创建收集账号执行" - -#: assets/models/automations/change_secret.py:15 assets/models/base.py:67 -#: assets/serializers/account/account.py:112 assets/serializers/base.py:13 -#: authentication/serializers/connect_token_secret.py:39 -#: authentication/serializers/connect_token_secret.py:49 -msgid "Secret type" -msgstr "密文类型" - -#: assets/models/automations/change_secret.py:19 -#: assets/serializers/automations/change_secret.py:25 -msgid "Secret strategy" -msgstr "密文策略" - -#: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:57 assets/models/base.py:69 -#: assets/serializers/base.py:16 authentication/models/temp_token.py:10 -#: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:19 -msgid "Secret" -msgstr "密钥" - -#: assets/models/automations/change_secret.py:22 -msgid "Password rules" -msgstr "密码规则" - -#: assets/models/automations/change_secret.py:25 -msgid "SSH key change strategy" -msgstr "SSH 密钥策略" - -#: assets/models/automations/change_secret.py:27 assets/models/backup.py:25 -#: assets/serializers/account/backup.py:30 -#: assets/serializers/automations/change_secret.py:40 -msgid "Recipient" -msgstr "收件人" - -#: assets/models/automations/change_secret.py:34 -msgid "Change secret automation" -msgstr "自动化改密" - -#: assets/models/automations/change_secret.py:56 -msgid "Old secret" -msgstr "原密码" - -#: assets/models/automations/change_secret.py:58 -msgid "Date started" -msgstr "开始日期" - -#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 -msgid "Error" -msgstr "错误" - -#: assets/models/automations/change_secret.py:64 -msgid "Change secret record" -msgstr "改密记录" - -#: assets/models/automations/gather_accounts.py:15 -#: assets/tasks/gather_accounts.py:28 -msgid "Gather asset accounts" -msgstr "收集账号" - #: assets/models/automations/gather_facts.py:15 msgid "Gather asset facts" msgstr "收集资产信息" @@ -894,57 +1272,15 @@ msgstr "收集资产信息" msgid "Ping asset" msgstr "测试资产" -#: assets/models/automations/push_account.py:16 -msgid "Push asset account" -msgstr "账号推送" - -#: assets/models/automations/verify_account.py:15 -msgid "Verify asset account" -msgstr "账号验证" - -#: assets/models/backup.py:34 assets/models/backup.py:92 -msgid "Account backup plan" -msgstr "账号备份计划" - -#: assets/models/backup.py:76 -#: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:186 -msgid "Time" -msgstr "时间" - -#: assets/models/backup.py:80 -msgid "Account backup snapshot" -msgstr "账号备份快照" - -#: assets/models/backup.py:87 audits/models.py:124 -#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 -msgid "Reason" -msgstr "原因" - -#: assets/models/backup.py:89 -#: assets/serializers/automations/change_secret.py:99 -#: assets/serializers/automations/change_secret.py:124 -#: terminal/serializers/session.py:44 -msgid "Is success" -msgstr "是否成功" - -#: assets/models/backup.py:96 -msgid "Account backup execution" -msgstr "账号备份执行" - -#: assets/models/base.py:26 +#: assets/models/base.py:19 msgid "Connectivity" msgstr "可连接性" -#: assets/models/base.py:28 authentication/models/temp_token.py:12 +#: assets/models/base.py:21 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "校验日期" -#: assets/models/base.py:70 -msgid "Privileged" -msgstr "特权账号" - -#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:60 +#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 #: perms/serializers/permission.py:25 users/models/group.py:25 #: users/models/user.py:681 msgid "User group" @@ -974,44 +1310,14 @@ msgstr "过滤器" msgid "Command filter rule" msgstr "命令过滤规则" -#: assets/models/gateway.py:40 assets/serializers/domain.py:16 +#: assets/models/favorite_asset.py:17 +msgid "Favorite Asset" +msgstr "收藏的资产" + +#: assets/models/gateway.py:35 assets/serializers/domain.py:16 msgid "Gateway" msgstr "网关" -#: assets/models/gateway.py:62 authentication/models/connection_token.py:104 -msgid "No account" -msgstr "没有账号" - -#: assets/models/gateway.py:84 -#, python-brace-format -msgid "Unable to connect to port {port} on {address}" -msgstr "无法连接到 {port} 上的端口 {address}" - -#: assets/models/gateway.py:87 authentication/middleware.py:76 -#: xpack/plugins/cloud/providers/fc.py:48 -msgid "Authentication failed" -msgstr "认证失败" - -#: assets/models/gateway.py:89 assets/models/gateway.py:116 -msgid "Connect failed" -msgstr "连接失败" - -#: assets/models/gathered_user.py:14 -msgid "Present" -msgstr "存在" - -#: assets/models/gathered_user.py:15 -msgid "Date last login" -msgstr "最后登录日期" - -#: assets/models/gathered_user.py:16 -msgid "IP last login" -msgstr "最后登录IP" - -#: assets/models/gathered_user.py:27 -msgid "GatherUser" -msgstr "收集用户" - #: assets/models/group.py:30 msgid "Asset group" msgstr "资产组" @@ -1033,14 +1339,14 @@ msgstr "系统" #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:25 #: authentication/serializers/connect_token_secret.py:114 -#: common/drf/serializers/common.py:82 settings/models.py:34 +#: common/serializers/common.py:82 settings/models.py:34 msgid "Value" msgstr "值" -#: assets/models/label.py:40 assets/serializers/cagegory.py:6 -#: assets/serializers/cagegory.py:13 +#: assets/models/label.py:40 assets/serializers/asset/common.py:123 +#: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: authentication/serializers/connect_token_secret.py:113 -#: common/drf/serializers/common.py:81 settings/serializers/sms.py:7 +#: common/serializers/common.py:81 settings/serializers/sms.py:7 msgid "Label" msgstr "标签" @@ -1052,7 +1358,7 @@ msgstr "新节点" msgid "empty" msgstr "空" -#: assets/models/node.py:551 perms/models/perm_node.py:27 +#: assets/models/node.py:551 perms/models/perm_node.py:28 msgid "Key" msgstr "键" @@ -1060,12 +1366,12 @@ msgstr "键" msgid "Full value" msgstr "全称" -#: assets/models/node.py:557 perms/models/perm_node.py:29 +#: assets/models/node.py:557 perms/models/perm_node.py:30 msgid "Parent key" msgstr "ssh私钥" #: assets/models/node.py:566 perms/serializers/permission.py:28 -#: xpack/plugins/cloud/models.py:96 +#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "节点" @@ -1082,8 +1388,8 @@ msgstr "必须的" msgid "Setting" msgstr "设置" -#: assets/models/platform.py:41 audits/const.py:69 settings/models.py:37 -#: terminal/serializers/applet_host.py:26 +#: assets/models/platform.py:41 audits/const.py:40 settings/models.py:37 +#: terminal/serializers/applet_host.py:28 msgid "Enabled" msgstr "启用" @@ -1099,162 +1405,106 @@ msgstr "启用资产探活" msgid "Ping method" msgstr "资产探活方式" -#: assets/models/platform.py:45 assets/models/platform.py:55 +#: assets/models/platform.py:45 assets/models/platform.py:58 msgid "Gather facts enabled" msgstr "收集资产信息" -#: assets/models/platform.py:46 assets/models/platform.py:57 +#: assets/models/platform.py:46 assets/models/platform.py:60 msgid "Gather facts method" msgstr "收集信息方式" #: assets/models/platform.py:47 +#, fuzzy +#| msgid "Change secret record" +msgid "Change secret enabled" +msgstr "改密记录" + +#: assets/models/platform.py:49 +#, fuzzy +#| msgid "Change secret record" +msgid "Change secret method" +msgstr "改密记录" + +#: assets/models/platform.py:51 msgid "Push account enabled" msgstr "启用账号推送" -#: assets/models/platform.py:48 +#: assets/models/platform.py:53 msgid "Push account method" msgstr "账号推送方式" -#: assets/models/platform.py:49 -msgid "Change password enabled" -msgstr "开启账号改密" - -#: assets/models/platform.py:51 -msgid "Change password method" -msgstr "更改密码方式" - -#: assets/models/platform.py:52 +#: assets/models/platform.py:55 msgid "Verify account enabled" msgstr "开启账号验证" -#: assets/models/platform.py:54 +#: assets/models/platform.py:57 msgid "Verify account method" msgstr "账号验证方式" -#: assets/models/platform.py:74 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:77 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:75 +#: assets/models/platform.py:78 msgid "Internal" msgstr "内置" -#: assets/models/platform.py:79 assets/serializers/platform.py:84 +#: assets/models/platform.py:82 assets/serializers/platform.py:84 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:81 +#: assets/models/platform.py:84 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:82 +#: assets/models/platform.py:85 msgid "Protocols enabled" msgstr "启用协议" -#: assets/models/platform.py:84 +#: assets/models/platform.py:87 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:85 +#: assets/models/platform.py:88 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:87 assets/serializers/platform.py:91 +#: assets/models/platform.py:90 assets/serializers/platform.py:91 msgid "Automation" msgstr "自动化" -#: assets/models/utils.py:19 +#: assets/models/utils.py:18 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/notifications.py:8 -msgid "Notification of account backup route task results" -msgstr "账号备份任务结果通知" - -#: assets/notifications.py:18 -msgid "" -"{} - The account backup passage task has been completed. See the attachment " -"for details" -msgstr "{} - 账号备份任务已完成, 详情见附件" - -#: assets/notifications.py:20 -msgid "" -"{} - The account backup passage task has been completed: the encryption " -"password has not been set - please go to personal information -> file " -"encryption password to set the encryption password" -msgstr "" -"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" -"置加密密码" - -#: assets/notifications.py:31 -msgid "Notification of implementation result of encryption change plan" -msgstr "改密计划任务结果通知" - -#: assets/notifications.py:41 -msgid "" -"{} - The encryption change task has been completed. See the attachment for " -"details" -msgstr "{} - 改密任务已完成, 详情见附件" - -#: assets/notifications.py:42 -msgid "" -"{} - The encryption change task has been completed: the encryption password " -"has not been set - please go to personal information -> file encryption " -"password to set the encryption password" -msgstr "" -"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" -"密密码" - -#: assets/serializers/account/account.py:18 -msgid "Push now" -msgstr "立即推送" - -#: assets/serializers/account/account.py:20 -#: assets/serializers/account/base.py:13 -msgid "Has secret" -msgstr "已托管密码" - -#: assets/serializers/account/account.py:27 -msgid "Account template not found" -msgstr "账号模版未找到" - -#: assets/serializers/account/account.py:72 -msgid "Asset not found" -msgstr "资产不存在" - -#: assets/serializers/account/backup.py:29 -#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 -#: settings/serializers/auth/ldap.py:66 -msgid "Periodic perform" -msgstr "定时执行" - -#: assets/serializers/account/backup.py:31 -#: assets/serializers/automations/change_secret.py:41 -msgid "Currently only mail sending is supported" -msgstr "当前只支持邮件发送" - -#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:89 -#: authentication/serializers/connect_token_secret.py:27 +#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:89 +#: authentication/serializers/connect_token_secret.py:28 #: authentication/serializers/connect_token_secret.py:65 -#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:107 +#: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:107 #: xpack/plugins/cloud/serializers/task.py:38 msgid "Protocols" msgstr "协议组" -#: assets/serializers/asset/common.py:88 +#: assets/serializers/asset/common.py:126 +#, fuzzy +#| msgid "Enabled" +msgid "Enabled info" +msgstr "启用" + +#: assets/serializers/asset/common.py:144 msgid "Address" msgstr "地址" -#: assets/serializers/asset/common.py:89 +#: assets/serializers/asset/common.py:145 msgid "Node path" msgstr "节点路径" -#: assets/serializers/asset/common.py:157 +#: assets/serializers/asset/common.py:205 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:173 +#: assets/serializers/asset/common.py:221 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" @@ -1319,34 +1569,6 @@ msgstr "主机名原始" msgid "Asset number" msgstr "资产编号" -#: assets/serializers/automations/change_secret.py:28 -msgid "SSH Key strategy" -msgstr "SSH 密钥策略" - -#: assets/serializers/automations/change_secret.py:70 -msgid "* Please enter the correct password length" -msgstr "* 请输入正确的密码长度" - -#: assets/serializers/automations/change_secret.py:73 -msgid "* Password length range 6-30 bits" -msgstr "* 密码长度范围 6-30 位" - -#: assets/serializers/automations/change_secret.py:117 -#: assets/serializers/automations/change_secret.py:145 audits/const.py:74 -#: audits/models.py:40 common/const/choices.py:18 ops/const.py:50 -#: ops/serializers/celery.py:39 terminal/models/session/sharing.py:103 -#: tickets/views/approve.py:114 -msgid "Success" -msgstr "成功" - -#: assets/serializers/automations/gather_accounts.py:23 -msgid "Executed amount" -msgstr "执行次数" - -#: assets/serializers/base.py:21 -msgid "Key password" -msgstr "密钥密码" - #: assets/serializers/cagegory.py:9 msgid "Constraints" msgstr "" @@ -1355,9 +1577,9 @@ msgstr "" msgid "Types" msgstr "类型" -#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:9 -msgid "Hostname" -msgstr "主机名" +#: assets/serializers/gateway.py:24 common/validators.py:32 +msgid "This field must be unique." +msgstr "字段必须唯一" #: assets/serializers/label.py:12 msgid "Assets amount" @@ -1387,51 +1609,29 @@ msgstr "SFTP根路径" msgid "Primary" msgstr "主要的" -#: assets/serializers/utils.py:13 -msgid "Password can not contains `{{` " -msgstr "密码不能包含 `{{` 字符" - -#: assets/serializers/utils.py:16 -msgid "Password can not contains `'` " -msgstr "密码不能包含 `'` 字符" - -#: assets/serializers/utils.py:18 -msgid "Password can not contains `\"` " -msgstr "密码不能包含 `\"` 字符" - -#: assets/serializers/utils.py:24 -msgid "private key invalid or passphrase error" -msgstr "密钥不合法或密钥密码错误" - #: assets/tasks/automation.py:11 -msgid "Execute automation" +#, fuzzy +#| msgid "Execute automation" +msgid "Asset execute automation" msgstr "执行自动化任务" -#: assets/tasks/backup.py:13 -msgid "Execute account backup plan" -msgstr "执行账号备份计划" - -#: assets/tasks/gather_accounts.py:31 -msgid "Gather assets accounts" -msgstr "收集资产上的账号" - -#: assets/tasks/gather_facts.py:26 +#: assets/tasks/gather_facts.py:23 msgid "Update some assets hardware info. " msgstr "更新资产硬件信息. " -#: assets/tasks/gather_facts.py:44 +#: assets/tasks/gather_facts.py:53 msgid "Manually update the hardware information of assets" msgstr "手动更新资产信息" -#: assets/tasks/gather_facts.py:49 +#: assets/tasks/gather_facts.py:57 msgid "Update assets hardware info: " msgstr "更新资产硬件信息" -#: assets/tasks/gather_facts.py:53 +#: assets/tasks/gather_facts.py:61 msgid "Manually update the hardware information of assets under a node" msgstr "手动更新节点下资产信息" -#: assets/tasks/gather_facts.py:59 +#: assets/tasks/gather_facts.py:65 msgid "Update node asset hardware information: " msgstr "更新节点资产硬件信息: " @@ -1448,26 +1648,24 @@ msgstr "自检程序已经在运行,不能重复启动" msgid "Periodic check the amount of assets under the node" msgstr "周期性检查节点下资产数量" -#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 +#: assets/tasks/ping.py:37 assets/tasks/ping.py:54 msgid "Test assets connectivity " msgstr "测试资产可连接性" -#: assets/tasks/ping.py:33 -msgid "Manually test the connectivity of a asset" +#: assets/tasks/ping.py:50 +#, fuzzy +#| msgid "Manually test the connectivity of a asset" +msgid "Manually test the connectivity of a asset" msgstr "手动测试资产连接性" -#: assets/tasks/ping.py:43 +#: assets/tasks/ping.py:58 msgid "Manually test the connectivity of assets under a node" msgstr "手动测试节点下资产连接性" -#: assets/tasks/ping.py:49 +#: assets/tasks/ping.py:62 msgid "Test if the assets under the node are connectable " msgstr "测试节点下资产是否可连接" -#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:34 -msgid "Push accounts to assets" -msgstr "推送账号到资产" - #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "资产已经被禁用, 跳过: {}" @@ -1484,14 +1682,6 @@ msgstr "为了安全,禁止推送用户 {}" msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks/verify_account.py:30 -msgid "Verify asset account availability" -msgstr "" - -#: assets/tasks/verify_account.py:37 -msgid "Verify accounts connectivity" -msgstr "测试账号可连接性" - #: audits/apps.py:9 msgid "Audits" msgstr "日志审计" @@ -1500,78 +1690,92 @@ msgstr "日志审计" msgid "The text content is too long. Use Elasticsearch to store operation logs" msgstr "文字内容太长。请使用 Elasticsearch 存储操作日志" -#: audits/backends/db.py:24 audits/backends/db.py:26 +#: audits/backends/db.py:25 audits/backends/db.py:27 msgid "Tips" msgstr "提示" -#: audits/const.py:45 +#: audits/const.py:12 msgid "Mkdir" msgstr "创建目录" -#: audits/const.py:46 +#: audits/const.py:13 msgid "Rmdir" msgstr "删除目录" -#: audits/const.py:47 audits/const.py:57 +#: audits/const.py:14 audits/const.py:24 #: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:232 +#: rbac/tree.py:231 msgid "Delete" msgstr "删除" -#: audits/const.py:48 perms/const.py:13 +#: audits/const.py:15 perms/const.py:13 msgid "Upload" msgstr "上传文件" -#: audits/const.py:49 +#: audits/const.py:16 msgid "Rename" msgstr "重命名" -#: audits/const.py:50 +#: audits/const.py:17 msgid "Symlink" msgstr "建立软链接" -#: audits/const.py:51 perms/const.py:14 +#: audits/const.py:18 perms/const.py:14 msgid "Download" msgstr "下载文件" -#: audits/const.py:55 rbac/tree.py:230 +#: audits/const.py:22 rbac/tree.py:229 msgid "View" msgstr "查看" -#: audits/const.py:56 rbac/tree.py:231 templates/_csv_import_export.html:18 +#: audits/const.py:23 rbac/tree.py:230 templates/_csv_import_export.html:18 #: templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: audits/const.py:58 +#: audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:229 +#: rbac/tree.py:228 msgid "Create" msgstr "创建" -#: audits/const.py:63 settings/serializers/terminal.py:6 +#: audits/const.py:27 perms/const.py:12 +msgid "Connect" +msgstr "连接" + +#: audits/const.py:28 authentication/templates/authentication/login.html:254 +#: authentication/templates/authentication/login.html:327 +#: templates/_header_bar.html:89 +msgid "Login" +msgstr "登录" + +#: audits/const.py:29 ops/const.py:9 +msgid "Change password" +msgstr "改密" + +#: audits/const.py:34 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:24 terminal/models/component/terminal.py:156 msgid "Terminal" msgstr "终端" -#: audits/const.py:70 +#: audits/const.py:41 msgid "-" msgstr "-" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "Yes" msgstr "是" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "No" msgstr "否" -#: audits/models.py:32 audits/models.py:55 audits/models.py:96 +#: audits/models.py:32 audits/models.py:59 audits/models.py:102 #: terminal/models/session/session.py:37 terminal/models/session/sharing.py:95 msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:37 audits/serializers.py:30 +#: audits/models.py:37 audits/serializers.py:32 msgid "Operate" msgstr "操作" @@ -1583,104 +1787,116 @@ msgstr "文件名" msgid "File transfer log" msgstr "文件管理" -#: audits/models.py:53 audits/serializers.py:84 +#: audits/models.py:53 audits/serializers.py:86 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:54 +#: audits/models.py:54 audits/models.py:57 msgid "Resource" msgstr "资源" -#: audits/models.py:56 audits/models.py:98 -#: terminal/backends/command/serializers.py:40 +#: audits/models.py:60 audits/models.py:104 +#: terminal/backends/command/serializers.py:41 msgid "Datetime" msgstr "日期" -#: audits/models.py:88 +#: audits/models.py:63 +#, fuzzy +#| msgid "Is active" +msgid "Is Activity" +msgstr "激活" + +#: audits/models.py:93 msgid "Operate log" msgstr "操作日志" -#: audits/models.py:94 +#: audits/models.py:100 msgid "Change by" msgstr "修改者" -#: audits/models.py:104 +#: audits/models.py:110 msgid "Password change log" msgstr "改密日志" -#: audits/models.py:111 +#: audits/models.py:117 msgid "Login type" msgstr "登录方式" -#: audits/models.py:113 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:119 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:115 +#: audits/models.py:121 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "登录城市" -#: audits/models.py:118 audits/serializers.py:60 +#: audits/models.py:124 audits/serializers.py:62 msgid "User agent" msgstr "用户代理" -#: audits/models.py:121 audits/serializers.py:44 +#: audits/models.py:127 audits/serializers.py:46 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms/profile.py:65 users/models/user.py:698 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:131 +#: audits/models.py:137 msgid "Date login" msgstr "登录日期" -#: audits/models.py:133 audits/serializers.py:62 +#: audits/models.py:139 audits/serializers.py:64 msgid "Authentication backend" msgstr "认证方式" -#: audits/models.py:174 +#: audits/models.py:180 msgid "User login log" msgstr "用户登录日志" -#: audits/serializers.py:61 +#: audits/serializers.py:63 msgid "Reason display" msgstr "原因描述" -#: audits/signal_handlers.py:48 +#: audits/serializers.py:112 +#, fuzzy +#| msgid "User {} {} it." +msgid "User {} {} this resource." +msgstr "用户 {} {}了它." + +#: audits/signal_handlers.py:50 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signal_handlers.py:50 settings/serializers/auth/sso.py:10 +#: audits/signal_handlers.py:52 settings/serializers/auth/sso.py:10 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:53 msgid "Auth Token" msgstr "认证令牌" -#: audits/signal_handlers.py:52 authentication/notifications.py:73 -#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: audits/signal_handlers.py:54 authentication/notifications.py:73 +#: authentication/views/login.py:73 authentication/views/wecom.py:177 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:736 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers.py:53 authentication/views/feishu.py:145 +#: audits/signal_handlers.py:55 authentication/views/feishu.py:144 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 users/models/user.py:738 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers.py:54 authentication/views/dingtalk.py:180 +#: audits/signal_handlers.py:56 authentication/views/dingtalk.py:179 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:737 msgid "DingTalk" msgstr "钉钉" -#: audits/signal_handlers.py:55 authentication/models/temp_token.py:16 +#: audits/signal_handlers.py:57 authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "临时密码" @@ -1688,6 +1904,26 @@ msgstr "临时密码" msgid "This action require verify your MFA" msgstr "此操作需要验证您的 MFA" +#: authentication/api/connection_token.py:264 +#, fuzzy +#| msgid "Account template not found" +msgid "Account not found" +msgstr "账号模版未找到" + +#: authentication/api/connection_token.py:267 +#, fuzzy +#| msgid "Permission name" +msgid "Permission Expired" +msgstr "授权规则名称" + +#: authentication/api/connection_token.py:279 +msgid "ACL action is reject" +msgstr "" + +#: authentication/api/connection_token.py:283 +msgid "ACL action is review" +msgstr "" + #: authentication/api/mfa.py:59 msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" @@ -1719,7 +1955,7 @@ msgstr "忘记密码" #: authentication/apps.py:7 settings/serializers/auth/base.py:10 #: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 #: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 -#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/oauth2.py:18 settings/serializers/auth/oidc.py:12 #: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 #: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 msgid "Authentication" @@ -1911,21 +2147,21 @@ msgstr "手机号没有设置" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:79 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237 -#: authentication/views/wecom.py:291 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:236 +#: authentication/views/wecom.py:290 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 -#: authentication/views/dingtalk.py:297 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 +#: authentication/views/dingtalk.py:296 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 msgid "FeiShu is not bound" msgstr "没有绑定飞书" @@ -2078,33 +2314,44 @@ msgid "Asset display" msgstr "资产名称" #: authentication/models/connection_token.py:41 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:73 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 #: tickets/models/ticket/apply_application.py:31 #: tickets/models/ticket/apply_asset.py:20 users/models/user.py:719 msgid "Date expired" msgstr "失效日期" #: authentication/models/connection_token.py:45 +#: perms/models/asset_permission.py:77 +msgid "From ticket" +msgstr "来自工单" + +#: authentication/models/connection_token.py:51 msgid "Connection token" msgstr "连接令牌" -#: authentication/models/connection_token.py:47 +#: authentication/models/connection_token.py:53 msgid "Can view connection token secret" msgstr "可以查看连接令牌密文" -#: authentication/models/connection_token.py:94 +#: authentication/models/connection_token.py:100 +#, fuzzy +#| msgid "Connection token" +msgid "Connection token inactive" +msgstr "连接令牌" + +#: authentication/models/connection_token.py:103 msgid "Connection token expired at: {}" msgstr "连接令牌过期: {}" -#: authentication/models/connection_token.py:97 +#: authentication/models/connection_token.py:106 msgid "No user or invalid user" msgstr "没有用户或用户失效" -#: authentication/models/connection_token.py:101 +#: authentication/models/connection_token.py:110 msgid "No asset or inactive asset" msgstr "没有资产或资产未激活" -#: authentication/models/connection_token.py:248 +#: authentication/models/connection_token.py:257 msgid "Super connection token" msgstr "超级连接令牌" @@ -2148,9 +2395,9 @@ msgstr "组件" msgid "Expired now" msgstr "立刻过期" -#: authentication/serializers/connect_token_secret.py:146 +#: authentication/serializers/connect_token_secret.py:147 #: authentication/templates/authentication/_access_key_modal.html:30 -#: perms/models/perm_node.py:20 users/serializers/group.py:35 +#: perms/models/perm_node.py:21 users/serializers/group.py:35 msgid "ID" msgstr "ID" @@ -2158,6 +2405,12 @@ msgstr "ID" msgid "Expired time" msgstr "过期时间" +#: authentication/serializers/connection_token.py:18 +#, fuzzy +#| msgid "Ticket flow" +msgid "Ticket info" +msgstr "工单流程" + #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 @@ -2174,7 +2427,7 @@ msgid "The {} cannot be empty" msgstr "{} 不能为空" #: authentication/serializers/token.py:79 perms/serializers/permission.py:30 -#: perms/serializers/permission.py:61 users/serializers/user.py:159 +#: perms/serializers/permission.py:62 users/serializers/user.py:159 msgid "Is valid" msgstr "账号是否有效" @@ -2355,12 +2608,6 @@ msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能 msgid "Cancel" msgstr "取消" -#: authentication/templates/authentication/login.html:254 -#: authentication/templates/authentication/login.html:327 -#: templates/_header_bar.html:89 -msgid "Login" -msgstr "登录" - #: authentication/templates/authentication/login.html:334 msgid "More login options" msgstr "其他方式登录" @@ -2402,73 +2649,73 @@ msgstr "复制成功" msgid "LAN" msgstr "局域网" -#: authentication/views/dingtalk.py:42 +#: authentication/views/dingtalk.py:41 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:45 +#: authentication/views/dingtalk.py:44 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 -#: authentication/views/wecom.py:56 +#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:55 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" -#: authentication/views/dingtalk.py:81 +#: authentication/views/dingtalk.py:80 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 +#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:147 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:165 +#: authentication/views/dingtalk.py:164 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:174 +#: authentication/views/dingtalk.py:173 msgid "The DingTalk is already bound to another user" msgstr "该钉钉已经绑定其他用户" -#: authentication/views/dingtalk.py:181 +#: authentication/views/dingtalk.py:180 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 +#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 +#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:40 +#: authentication/views/feishu.py:39 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:88 +#: authentication/views/feishu.py:87 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:130 +#: authentication/views/feishu.py:129 msgid "FeiShu query user failed" msgstr "飞书查询用户失败" -#: authentication/views/feishu.py:139 +#: authentication/views/feishu.py:138 msgid "The FeiShu is already bound to another user" msgstr "该飞书已经绑定其他用户" -#: authentication/views/feishu.py:146 +#: authentication/views/feishu.py:145 msgid "Binding FeiShu successfully" msgstr "绑定 飞书 成功" -#: authentication/views/feishu.py:198 +#: authentication/views/feishu.py:197 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/feishu.py:205 +#: authentication/views/feishu.py:204 msgid "Please login with a password and then bind the FeiShu" msgstr "请使用密码登录,然后绑定飞书" @@ -2504,34 +2751,38 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:41 +#: authentication/views/wecom.py:40 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" -#: authentication/views/wecom.py:44 +#: authentication/views/wecom.py:43 msgid "WeCom Error" msgstr "企业微信错误" -#: authentication/views/wecom.py:163 +#: authentication/views/wecom.py:162 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:172 +#: authentication/views/wecom.py:171 msgid "The WeCom is already bound to another user" msgstr "该企业微信已经绑定其他用户" -#: authentication/views/wecom.py:179 +#: authentication/views/wecom.py:178 msgid "Binding WeCom successfully" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:231 authentication/views/wecom.py:285 +#: authentication/views/wecom.py:230 authentication/views/wecom.py:284 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:238 authentication/views/wecom.py:292 +#: authentication/views/wecom.py:237 authentication/views/wecom.py:291 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" +#: common/api/action.py:52 +msgid "Request file format may be wrong" +msgstr "上传的文件格式错误 或 其它类型资源的文件" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -2554,11 +2805,12 @@ msgstr "定时触发" msgid "Ready" msgstr "准备" -#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:39 +#: common/const/choices.py:16 terminal/const.py:58 tickets/const.py:29 +#: tickets/const.py:39 msgid "Pending" msgstr "待定的" -#: common/const/choices.py:17 ops/const.py:49 +#: common/const/choices.py:17 ops/const.py:50 msgid "Running" msgstr "运行中" @@ -2570,66 +2822,57 @@ msgstr "取消" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:94 +#: common/db/fields.py:97 msgid "Marshal dict data to char field" msgstr "编码 dict 为 char" -#: common/db/fields.py:98 +#: common/db/fields.py:101 msgid "Marshal dict data to text field" msgstr "编码 dict 为 text" -#: common/db/fields.py:110 +#: common/db/fields.py:113 msgid "Marshal list data to char field" msgstr "编码 list 为 char" -#: common/db/fields.py:114 +#: common/db/fields.py:117 msgid "Marshal list data to text field" msgstr "编码 list 为 text" -#: common/db/fields.py:118 +#: common/db/fields.py:121 msgid "Marshal data to char field" msgstr "编码数据为 char" -#: common/db/fields.py:122 +#: common/db/fields.py:125 msgid "Marshal data to text field" msgstr "编码数据为 text" -#: common/db/fields.py:164 +#: common/db/fields.py:167 msgid "Encrypt field using Secret Key" msgstr "加密的字段" -#: common/db/models.py:75 +#: common/db/mixins.py:32 +msgid "is discard" +msgstr "忽略的" + +#: common/db/mixins.py:33 +msgid "discard time" +msgstr "忽略时间" + +#: common/db/models.py:34 msgid "Updated by" msgstr "更新人" -#: common/drf/exc_handlers.py:25 +#: common/db/validators.py:9 +#, fuzzy +#| msgid "Invalid data type, should be list" +msgid "Invalid port range, should be like and within {}-{}" +msgstr "错误的数据类型,应该是列表" + +#: common/drf/exc_handlers.py:26 msgid "Object" msgstr "对象" -#: common/drf/fields.py:77 tickets/serializers/ticket/common.py:58 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "该字段是必填项。" - -#: common/drf/fields.py:78 -#, python-brace-format -msgid "Invalid pk \"{pk_value}\" - object does not exist." -msgstr "错误的 id \"{pk_value}\" - 对象不存在" - -#: common/drf/fields.py:79 -#, python-brace-format -msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "错误类型。期望 pk 值,收到 {data_type}。" - -#: common/drf/fields.py:141 -msgid "Invalid data type, should be list" -msgstr "错误的数据类型,应该是列表" - -#: common/drf/fields.py:156 -msgid "Invalid choice: {}" -msgstr "无效选项: {}" - -#: common/drf/metadata.py:130 +#: common/drf/metadata.py:127 msgid "Organization ID" msgstr "组织 ID" @@ -2641,14 +2884,6 @@ msgstr "文件内容太大 (最大长度 `{}` 字节)" msgid "Parse file error: {}" msgstr "解析文件错误: {}" -#: common/drf/serializers/common.py:86 -msgid "Children" -msgstr "节点" - -#: common/drf/serializers/common.py:94 -msgid "File" -msgstr "文件" - #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -2678,31 +2913,6 @@ msgstr "此操作需要确认当前用户" msgid "Unexpect error occur" msgstr "发生意外错误" -#: common/mixins/api/action.py:52 -msgid "Request file format may be wrong" -msgstr "上传的文件格式错误 或 其它类型资源的文件" - -#: common/mixins/models.py:32 -msgid "is discard" -msgstr "忽略的" - -#: common/mixins/models.py:33 -msgid "discard time" -msgstr "忽略时间" - -#: common/mixins/views.py:58 -msgid "Export all" -msgstr "导出所有" - -#: common/mixins/views.py:60 -msgid "Export only selected items" -msgstr "仅导出选择项" - -#: common/mixins/views.py:65 -#, python-format -msgid "Export filtered: %s" -msgstr "导出搜素: %s" - #: common/plugins/es.py:28 msgid "Invalid elasticsearch config" msgstr "无效的 Elasticsearch 配置" @@ -2767,6 +2977,37 @@ msgstr "验证码错误" msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" +#: common/serializers/common.py:86 +msgid "Children" +msgstr "节点" + +#: common/serializers/common.py:94 +msgid "File" +msgstr "文件" + +#: common/serializers/fields.py:100 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "该字段是必填项。" + +#: common/serializers/fields.py:101 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "错误的 id \"{pk_value}\" - 对象不存在" + +#: common/serializers/fields.py:102 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "错误类型。期望 pk 值,收到 {data_type}。" + +#: common/serializers/fields.py:172 +msgid "Invalid data type, should be list" +msgstr "错误的数据类型,应该是列表" + +#: common/serializers/fields.py:187 +msgid "Invalid choice: {}" +msgstr "无效选项: {}" + #: common/tasks.py:13 msgid "Send email" msgstr "发件邮件" @@ -2787,10 +3028,6 @@ msgstr "无效地址" msgid "Special char not allowed" msgstr "不能包含特殊字符" -#: common/validators.py:32 -msgid "This field must be unique." -msgstr "字段必须唯一" - #: common/validators.py:40 msgid "Should not contains special characters" msgstr "不能包含特殊字符" @@ -2799,6 +3036,19 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" +#: common/views/mixins.py:57 +msgid "Export all" +msgstr "导出所有" + +#: common/views/mixins.py:59 +msgid "Export only selected items" +msgstr "仅导出选择项" + +#: common/views/mixins.py:64 +#, python-format +msgid "Export filtered: %s" +msgstr "导出搜素: %s" + #: jumpserver/conf.py:415 msgid "Create account successfully" msgstr "创建账号成功" @@ -2882,7 +3132,7 @@ msgstr "跳过以下主机: " msgid "Waiting task start" msgstr "等待任务开始" -#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:55 +#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:56 msgid "App ops" msgstr "作业中心" @@ -2898,19 +3148,23 @@ msgstr "校验" msgid "Collect" msgstr "收集" -#: ops/const.py:9 -msgid "Change password" -msgstr "改密" - #: ops/const.py:19 msgid "Custom password" msgstr "自定义密码" +#: ops/const.py:20 +msgid "All assets use the same random password" +msgstr "随机一个" + +#: ops/const.py:21 +msgid "All assets use different random password" +msgstr "各自随机" + #: ops/const.py:33 msgid "Adhoc" msgstr "命令" -#: ops/const.py:34 ops/models/job.py:31 +#: ops/const.py:34 ops/models/job.py:32 msgid "Playbook" msgstr "Playbook" @@ -2926,10 +3180,20 @@ msgstr "特权账号优先" msgid "Skip" msgstr "跳过" -#: ops/const.py:45 ops/models/adhoc.py:20 +#: ops/const.py:45 msgid "Powershell" msgstr "PowerShell" +#: ops/const.py:46 +msgid "Python" +msgstr "" + +#: ops/const.py:52 +#, fuzzy +#| msgid "Test timeout" +msgid "Timeout" +msgstr "测试超时时间" + #: ops/exception.py:6 msgid "no valid program entry found." msgstr "没有可用程序入口" @@ -2959,26 +3223,26 @@ msgstr "输入在 {} - {} 范围之间" msgid "Require periodic or regularly perform setting" msgstr "需要周期或定期设置" -#: ops/models/adhoc.py:24 +#: ops/models/adhoc.py:23 msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:26 ops/models/job.py:28 +#: ops/models/adhoc.py:25 ops/models/job.py:29 msgid "Module" msgstr "模块" -#: ops/models/adhoc.py:27 ops/models/celery.py:55 ops/models/job.py:26 +#: ops/models/adhoc.py:26 ops/models/celery.py:58 ops/models/job.py:27 #: terminal/models/component/task.py:16 msgid "Args" msgstr "参数" -#: ops/models/adhoc.py:28 ops/models/base.py:16 ops/models/base.py:53 -#: ops/models/job.py:33 ops/models/job.py:104 ops/models/playbook.py:16 +#: ops/models/adhoc.py:27 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:34 ops/models/job.py:105 ops/models/playbook.py:16 #: terminal/models/session/sharing.py:23 msgid "Creator" msgstr "创建者" -#: ops/models/adhoc.py:46 +#: ops/models/adhoc.py:45 msgid "AdHoc" msgstr "任务各版本" @@ -2990,83 +3254,92 @@ msgstr "账号策略" msgid "Last execution" msgstr "最后执行" -#: ops/models/base.py:22 +#: ops/models/base.py:22 ops/serializers/job.py:16 msgid "Date last run" msgstr "最后运行日期" -#: ops/models/base.py:51 ops/models/job.py:102 +#: ops/models/base.py:51 ops/models/job.py:103 #: xpack/plugins/cloud/models.py:170 msgid "Result" msgstr "结果" -#: ops/models/base.py:52 ops/models/job.py:103 +#: ops/models/base.py:52 ops/models/job.py:104 msgid "Summary" msgstr "汇总" +#: ops/models/celery.py:16 +msgid "Date last publish" +msgstr "发布日期" + #: ops/models/celery.py:47 msgid "Celery Task" msgstr "Celery 任务" -#: ops/models/celery.py:56 terminal/models/component/task.py:17 +#: ops/models/celery.py:50 +msgid "Can view task monitor" +msgstr "可以查看任务监控" + +#: ops/models/celery.py:59 terminal/models/component/task.py:17 msgid "Kwargs" msgstr "其它参数" -#: ops/models/celery.py:57 tickets/models/comment.py:13 -#: tickets/models/ticket/general.py:44 tickets/models/ticket/general.py:279 +#: ops/models/celery.py:60 tickets/models/comment.py:13 +#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279 +#: tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状态" -#: ops/models/celery.py:58 terminal/models/session/sharing.py:110 +#: ops/models/celery.py:61 terminal/models/session/sharing.py:110 #: tickets/const.py:25 msgid "Finished" msgstr "结束" -#: ops/models/celery.py:59 +#: ops/models/celery.py:62 msgid "Date published" msgstr "发布日期" -#: ops/models/celery.py:83 +#: ops/models/celery.py:86 msgid "Celery Task Execution" msgstr "Celery 任务执行" -#: ops/models/job.py:29 +#: ops/models/job.py:30 msgid "Chdir" msgstr "运行目录" -#: ops/models/job.py:30 +#: ops/models/job.py:31 msgid "Timeout (Seconds)" -msgstr "超市时间(秒)" +msgstr "超时时间(秒)" -#: ops/models/job.py:35 +#: ops/models/job.py:36 msgid "Runas" msgstr "运行用户" -#: ops/models/job.py:37 +#: ops/models/job.py:38 msgid "Runas policy" msgstr "用户策略" -#: ops/models/job.py:38 +#: ops/models/job.py:39 msgid "Use Parameter Define" msgstr "" -#: ops/models/job.py:39 +#: ops/models/job.py:40 msgid "Parameters define" msgstr "" -#: ops/models/job.py:91 +#: ops/models/job.py:92 msgid "Job" msgstr "作业" -#: ops/models/job.py:101 +#: ops/models/job.py:102 msgid "Parameters" msgstr "" -#: ops/models/job.py:300 +#: ops/models/job.py:311 msgid "Job Execution" msgstr "作业执行" -#: ops/models/job.py:311 +#: ops/models/job.py:322 msgid "Job audit log" msgstr "作业审计日志" @@ -3102,13 +3375,17 @@ msgstr "CPU 使用率超过 {max_threshold}: => {value}" msgid "Run after save" msgstr "保存后执行" -#: ops/serializers/job.py:43 +#: ops/serializers/job.py:52 msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:44 -msgid "Material" -msgstr "" +#: ops/serializers/job.py:55 terminal/serializers/session.py:53 +msgid "Is finished" +msgstr "是否完成" + +#: ops/serializers/job.py:56 +msgid "Time cost" +msgstr "花费时间" #: ops/signal_handlers.py:74 terminal/models/applet/host.py:111 #: terminal/models/component/task.py:24 @@ -3147,10 +3424,6 @@ msgstr "周期检测服务性能" msgid "Task log" msgstr "任务列表" -#: ops/utils.py:64 -msgid "Update task content: {}" -msgstr "更新任务内容: {}" - #: ops/variables.py:24 msgid "The current user`s username of JumpServer" msgstr "" @@ -3187,17 +3460,17 @@ msgstr "" msgid "Name of the job" msgstr "" -#: orgs/api.py:67 +#: orgs/api.py:63 msgid "The current organization ({}) cannot be deleted" msgstr "当前组织 ({}) 不能被删除" -#: orgs/api.py:72 +#: orgs/api.py:68 msgid "" "LDAP synchronization is set to the current organization. Please switch to " "another organization before deleting" msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进行删除操作" -#: orgs/api.py:81 +#: orgs/api.py:78 msgid "The organization have resource ({}) cannot be deleted" msgstr "组织存在资源 ({}) 不能被删除" @@ -3205,10 +3478,10 @@ msgstr "组织存在资源 ({}) 不能被删除" msgid "App organizations" msgstr "组织管理" -#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:82 +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:84 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 -#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:62 +#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60 msgid "Organization" msgstr "组织" @@ -3216,27 +3489,27 @@ msgstr "组织" msgid "Org name" msgstr "组织名称" -#: orgs/models.py:68 rbac/models/role.py:36 terminal/models/applet/applet.py:28 +#: orgs/models.py:70 rbac/models/role.py:36 terminal/models/applet/applet.py:29 msgid "Builtin" msgstr "内置的" -#: orgs/models.py:74 +#: orgs/models.py:76 msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:76 +#: orgs/models.py:78 msgid "DEFAULT" msgstr "默认组织" -#: orgs/models.py:78 +#: orgs/models.py:80 msgid "SYSTEM" msgstr "系统组织" -#: orgs/models.py:84 +#: orgs/models.py:86 msgid "Can view root org" msgstr "可以查看全局组织" -#: orgs/models.py:85 +#: orgs/models.py:87 msgid "Can view all joined org" msgstr "可以查看所有加入的组织" @@ -3248,10 +3521,6 @@ msgstr "刷新组织缓存" msgid "App permissions" msgstr "授权管理" -#: perms/const.py:12 -msgid "Connect" -msgstr "连接" - #: perms/const.py:15 msgid "Copy" msgstr "复制" @@ -3268,46 +3537,42 @@ msgstr "文件传输" msgid "Clipboard" msgstr "剪贴板" -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:29 -#: perms/serializers/permission.py:59 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:29 +#: perms/serializers/permission.py:60 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "动作" -#: perms/models/asset_permission.py:76 -msgid "From ticket" -msgstr "来自工单" - -#: perms/models/asset_permission.py:82 +#: perms/models/asset_permission.py:83 msgid "Asset permission" msgstr "资产授权" -#: perms/models/perm_node.py:67 +#: perms/models/perm_node.py:68 msgid "Ungrouped" msgstr "未分组" -#: perms/models/perm_node.py:69 +#: perms/models/perm_node.py:70 msgid "Favorite" msgstr "收藏夹" -#: perms/models/perm_node.py:120 +#: perms/models/perm_node.py:121 msgid "Permed asset" msgstr "授权的资产" -#: perms/models/perm_node.py:122 +#: perms/models/perm_node.py:123 msgid "Can view my assets" msgstr "可以查看我的资产" -#: perms/models/perm_node.py:123 +#: perms/models/perm_node.py:124 msgid "Can view user assets" msgstr "可以查看用户授权的资产" -#: perms/models/perm_node.py:124 +#: perms/models/perm_node.py:125 msgid "Can view usergroup assets" msgstr "可以查看用户组授权的资产" -#: perms/models/perm_node.py:135 +#: perms/models/perm_node.py:136 msgid "Permed account" msgstr "授权账号" @@ -3331,7 +3596,7 @@ msgstr "资产授权规则将要过期" msgid "asset permissions of organization {}" msgstr "组织 ({}) 的资产授权" -#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: perms/serializers/permission.py:31 perms/serializers/permission.py:61 #: users/serializers/user.py:91 users/serializers/user.py:161 msgid "Is expired" msgstr "已过期" @@ -3372,27 +3637,27 @@ msgstr "{} 至少有一个系统角色" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:111 +#: rbac/builtin.py:109 msgid "SystemAdmin" msgstr "系统管理员" -#: rbac/builtin.py:114 +#: rbac/builtin.py:112 msgid "SystemAuditor" msgstr "系统审计员" -#: rbac/builtin.py:117 +#: rbac/builtin.py:115 msgid "SystemComponent" msgstr "系统组件" -#: rbac/builtin.py:123 +#: rbac/builtin.py:121 msgid "OrgAdmin" msgstr "组织管理员" -#: rbac/builtin.py:126 +#: rbac/builtin.py:124 msgid "OrgAuditor" msgstr "组织审计员" -#: rbac/builtin.py:129 +#: rbac/builtin.py:127 msgid "OrgUser" msgstr "组织用户" @@ -3425,7 +3690,7 @@ msgid "Permissions" msgstr "授权" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 -#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 +#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:36 msgid "Scope" msgstr "范围" @@ -3472,7 +3737,7 @@ msgstr "权限" msgid "Users amount" msgstr "用户数量" -#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:23 +#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:24 msgid "Display name" msgstr "显示名称" @@ -3524,24 +3789,24 @@ msgstr "备份账号" msgid "Gather account" msgstr "收集账号" -#: rbac/tree.py:51 +#: rbac/tree.py:52 msgid "Asset change auth" msgstr "资产改密" -#: rbac/tree.py:52 +#: rbac/tree.py:53 msgid "Terminal setting" msgstr "终端设置" -#: rbac/tree.py:53 +#: rbac/tree.py:54 msgid "Task Center" msgstr "任务中心" -#: rbac/tree.py:54 +#: rbac/tree.py:55 msgid "My assets" msgstr "我的资产" -#: rbac/tree.py:56 terminal/models/applet/applet.py:38 -#: terminal/models/applet/applet.py:127 terminal/models/applet/host.py:27 +#: rbac/tree.py:57 terminal/models/applet/applet.py:39 +#: terminal/models/applet/applet.py:133 terminal/models/applet/host.py:27 msgid "Applet" msgstr "远程应用" @@ -3561,10 +3826,6 @@ msgstr "一般设置" msgid "View permission tree" msgstr "查看授权树" -#: rbac/tree.py:124 -msgid "Execute batch command" -msgstr "执行批量命令" - #: settings/api/dingtalk.py:31 settings/api/feishu.py:36 #: settings/api/sms.py:148 settings/api/wecom.py:37 msgid "Test success" @@ -3706,7 +3967,7 @@ msgstr "服务端地址" msgid "Proxy server url" msgstr "回调地址" -#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:54 #: settings/serializers/auth/saml2.py:34 msgid "Logout completely" msgstr "同步注销" @@ -3768,7 +4029,7 @@ msgstr "用户过滤器" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:56 #: settings/serializers/auth/oidc.py:37 msgid "User attr map" msgstr "用户属性映射" @@ -3793,52 +4054,52 @@ msgstr "搜索分页数量" msgid "Enable LDAP auth" msgstr "启用 LDAP 认证" -#: settings/serializers/auth/oauth2.py:19 +#: settings/serializers/auth/oauth2.py:18 msgid "OAuth2" msgstr "OAuth2" -#: settings/serializers/auth/oauth2.py:22 +#: settings/serializers/auth/oauth2.py:21 msgid "Enable OAuth2 Auth" msgstr "启用 OAuth2 认证" -#: settings/serializers/auth/oauth2.py:25 +#: settings/serializers/auth/oauth2.py:24 msgid "Logo" msgstr "图标" -#: settings/serializers/auth/oauth2.py:28 +#: settings/serializers/auth/oauth2.py:27 msgid "Service provider" msgstr "服务提供商" -#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 +#: settings/serializers/auth/oauth2.py:30 settings/serializers/auth/oidc.py:19 msgid "Client Id" msgstr "客户端 ID" -#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 +#: settings/serializers/auth/oauth2.py:33 settings/serializers/auth/oidc.py:22 #: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "客户端密钥" -#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:68 +#: settings/serializers/auth/oauth2.py:39 settings/serializers/auth/oidc.py:68 msgid "Provider auth endpoint" msgstr "授权端点地址" -#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:71 +#: settings/serializers/auth/oauth2.py:42 settings/serializers/auth/oidc.py:71 msgid "Provider token endpoint" msgstr "token 端点地址" -#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 +#: settings/serializers/auth/oauth2.py:45 settings/serializers/auth/oidc.py:30 msgid "Client authentication method" msgstr "客户端认证方式" -#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:77 +#: settings/serializers/auth/oauth2.py:49 settings/serializers/auth/oidc.py:77 msgid "Provider userinfo endpoint" msgstr "用户信息端点地址" -#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:80 +#: settings/serializers/auth/oauth2.py:52 settings/serializers/auth/oidc.py:80 msgid "Provider end session endpoint" msgstr "注销会话端点地址" -#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:98 +#: settings/serializers/auth/oauth2.py:59 settings/serializers/auth/oidc.py:98 #: settings/serializers/auth/saml2.py:35 msgid "Always update user" msgstr "总是更新用户信息" @@ -3973,7 +4234,7 @@ msgstr "短信服务商 / 协议" #: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 #: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 -#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:69 msgid "Signature" msgstr "签名" @@ -4045,7 +4306,7 @@ msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过 msgid "SSO auth key TTL" msgstr "令牌有效期" -#: settings/serializers/auth/sso.py:17 +#: settings/serializers/auth/sso.py:17 settings/serializers/security.py:117 #: xpack/plugins/cloud/serializers/account_attrs.py:176 msgid "Unit: second" msgstr "单位: 秒" @@ -4222,7 +4483,7 @@ msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 你 msgid "Create user email content" msgstr "邮件的内容" -#: settings/serializers/email.py:65 +#: settings/serializers/email.py:66 #, python-brace-format msgid "" "Tips: When creating a user, send the content of the email, support " @@ -4230,7 +4491,7 @@ msgid "" msgstr "" "提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" -#: settings/serializers/email.py:69 +#: settings/serializers/email.py:70 msgid "Tips: Email signature (eg:jumpserver)" msgstr "邮件署名 (如:jumpserver)" @@ -4446,10 +4707,16 @@ msgid "" msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效" #: settings/serializers/security.py:116 +#, fuzzy +#| msgid "Verify code" +msgid "Verify code TTL" +msgstr "验证码" + +#: settings/serializers/security.py:121 msgid "Enable Login dynamic code" msgstr "启用登录附加码" -#: settings/serializers/security.py:117 +#: settings/serializers/security.py:122 msgid "" "The password and additional code are sent to a third party authentication " "system for verification" @@ -4457,93 +4724,93 @@ msgstr "" "密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密" "码+6位数字 完成认证" -#: settings/serializers/security.py:122 +#: settings/serializers/security.py:127 msgid "MFA in login page" msgstr "MFA 在登录页面输入" -#: settings/serializers/security.py:123 +#: settings/serializers/security.py:128 msgid "Eu security regulations(GDPR) require MFA to be on the login page" msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全" -#: settings/serializers/security.py:126 +#: settings/serializers/security.py:131 msgid "Enable Login captcha" msgstr "启用登录验证码" -#: settings/serializers/security.py:127 +#: settings/serializers/security.py:132 msgid "Enable captcha to prevent robot authentication" msgstr "开启验证码,防止机器人登录" -#: settings/serializers/security.py:146 +#: settings/serializers/security.py:151 msgid "Security" msgstr "安全" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:154 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/security.py:151 +#: settings/serializers/security.py:156 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/security.py:155 +#: settings/serializers/security.py:160 msgid "Enable watermark" msgstr "开启水印" -#: settings/serializers/security.py:156 +#: settings/serializers/security.py:161 msgid "Enabled, the web session and replay contains watermark information" msgstr "启用后,Web 会话和录像将包含水印信息" -#: settings/serializers/security.py:160 +#: settings/serializers/security.py:165 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/security.py:161 +#: settings/serializers/security.py:166 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/security.py:164 +#: settings/serializers/security.py:169 msgid "Remember manual auth" msgstr "保存手动输入密码" -#: settings/serializers/security.py:167 +#: settings/serializers/security.py:172 msgid "Enable change auth secure mode" msgstr "启用改密安全模式" -#: settings/serializers/security.py:170 +#: settings/serializers/security.py:175 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/security.py:173 +#: settings/serializers/security.py:178 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/security.py:174 +#: settings/serializers/security.py:179 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/security.py:177 -msgid "Batch command execution" -msgstr "批量命令执行" +#: settings/serializers/security.py:182 +msgid "Operation center" +msgstr "作业中心" -#: settings/serializers/security.py:178 +#: settings/serializers/security.py:183 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/security.py:181 +#: settings/serializers/security.py:186 msgid "Session share" msgstr "会话分享" -#: settings/serializers/security.py:182 +#: settings/serializers/security.py:187 msgid "Enabled, Allows user active session to be shared with other users" msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" -#: settings/serializers/security.py:185 +#: settings/serializers/security.py:190 msgid "Remote Login Protection" msgstr "异地登录保护" -#: settings/serializers/security.py:187 +#: settings/serializers/security.py:192 msgid "" "The system determines whether the login IP address belongs to a common login " "city. If the account is logged in from a common login city, the system sends " @@ -4552,6 +4819,10 @@ msgstr "" "根据登录 IP 是否所属常用登录城市进行判断,若账号在非常用城市登录,会发送异地" "登录提醒" +#: settings/serializers/terminal.py:9 +msgid "Hostname" +msgstr "主机名" + #: settings/serializers/terminal.py:15 msgid "Auto" msgstr "自动" @@ -4979,7 +5250,7 @@ msgid "Input" msgstr "输入" #: terminal/backends/command/models.py:24 -#: terminal/backends/command/serializers.py:38 +#: terminal/backends/command/serializers.py:39 msgid "Output" msgstr "输出" @@ -4999,22 +5270,22 @@ msgstr "风险等级" msgid "Session ID" msgstr "会话ID" -#: terminal/backends/command/serializers.py:37 +#: terminal/backends/command/serializers.py:38 msgid "Account " msgstr "账号" -#: terminal/backends/command/serializers.py:39 +#: terminal/backends/command/serializers.py:40 msgid "Timestamp" msgstr "时间戳" -#: terminal/backends/command/serializers.py:41 +#: terminal/backends/command/serializers.py:42 #: terminal/models/component/terminal.py:84 msgid "Remote Address" msgstr "远端地址" -#: terminal/connect_methods.py:46 terminal/connect_methods.py:47 -#: terminal/connect_methods.py:48 terminal/connect_methods.py:49 -#: terminal/connect_methods.py:50 +#: terminal/connect_methods.py:47 terminal/connect_methods.py:48 +#: terminal/connect_methods.py:49 terminal/connect_methods.py:50 +#: terminal/connect_methods.py:51 msgid "DB Client" msgstr "数据库客户端" @@ -5035,6 +5306,10 @@ msgstr "正常" msgid "Offline" msgstr "离线" +#: terminal/const.py:61 +msgid "Mismatch" +msgstr "" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "不支持批量创建" @@ -5043,19 +5318,24 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/applet/applet.py:25 +#: terminal/models/applet/applet.py:26 msgid "Author" msgstr "作者" -#: terminal/models/applet/applet.py:30 +#: terminal/models/applet/applet.py:31 msgid "Tags" msgstr "标签" -#: terminal/models/applet/applet.py:34 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:35 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "主机" -#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 +#: terminal/models/applet/applet.py:135 terminal/models/applet/host.py:33 +#: terminal/models/applet/host.py:105 +msgid "Hosting" +msgstr "宿主机" + +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:40 msgid "Deploy options" msgstr "部署参数" @@ -5071,47 +5351,55 @@ msgstr "初始化日期" msgid "Date synced" msgstr "同步日期" -#: terminal/models/applet/host.py:33 -msgid "Applet host" -msgstr "远程应用发布机" - -#: terminal/models/applet/host.py:105 -msgid "Hosting" -msgstr "宿主机" - #: terminal/models/applet/host.py:106 msgid "Initial" msgstr "初始化" #: terminal/models/component/endpoint.py:15 -msgid "HTTPS Port" +msgid "HTTPS port" msgstr "HTTPS 端口" #: terminal/models/component/endpoint.py:16 -msgid "HTTP Port" +msgid "HTTP port" msgstr "HTTP 端口" #: terminal/models/component/endpoint.py:17 -msgid "SSH Port" +msgid "SSH port" msgstr "SSH 端口" #: terminal/models/component/endpoint.py:18 -msgid "RDP Port" +msgid "RDP port" msgstr "RDP 端口" -#: terminal/models/component/endpoint.py:25 -#: terminal/models/component/endpoint.py:94 terminal/serializers/endpoint.py:57 +#: terminal/models/component/endpoint.py:19 +msgid "MySQL port" +msgstr "MySQL 端口" + +#: terminal/models/component/endpoint.py:20 +msgid "MariaDB port" +msgstr "MariaDB 端口" + +#: terminal/models/component/endpoint.py:21 +msgid "PostgreSQL port" +msgstr "PostgreSQL 端口" + +#: terminal/models/component/endpoint.py:22 +msgid "Redis port" +msgstr "Redis 端口" + +#: terminal/models/component/endpoint.py:29 +#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64 #: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 #: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 #: terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "端点" -#: terminal/models/component/endpoint.py:87 +#: terminal/models/component/endpoint.py:91 msgid "IP group" msgstr "IP 组" -#: terminal/models/component/endpoint.py:99 +#: terminal/models/component/endpoint.py:103 msgid "Endpoint rule" msgstr "端点规则" @@ -5277,73 +5565,63 @@ msgstr "级别" msgid "Batch danger command alert" msgstr "批量危险命令告警" -#: terminal/serializers/applet.py:16 -msgid "Published" -msgstr "已发布" - -#: terminal/serializers/applet.py:17 -msgid "Unpublished" -msgstr "未发布" - -#: terminal/serializers/applet.py:18 -msgid "Not match" -msgstr "没有匹配的" - -#: terminal/serializers/applet.py:32 +#: terminal/serializers/applet.py:27 msgid "Icon" msgstr "图标" -#: terminal/serializers/applet_host.py:21 +#: terminal/serializers/applet_host.py:23 msgid "Per Session" msgstr "每会话" -#: terminal/serializers/applet_host.py:22 +#: terminal/serializers/applet_host.py:24 msgid "Per Device" msgstr "每设备" -#: terminal/serializers/applet_host.py:28 +#: terminal/serializers/applet_host.py:30 msgid "RDS Licensing" msgstr "RDS 许可证" -#: terminal/serializers/applet_host.py:29 +#: terminal/serializers/applet_host.py:31 msgid "RDS License Server" msgstr "RDS 许可服务器" -#: terminal/serializers/applet_host.py:30 +#: terminal/serializers/applet_host.py:32 msgid "RDS Licensing Mode" msgstr "RDS 授权模式" -#: terminal/serializers/applet_host.py:32 +#: terminal/serializers/applet_host.py:34 msgid "RDS fSingleSessionPerUser" msgstr "" -#: terminal/serializers/applet_host.py:33 +#: terminal/serializers/applet_host.py:35 msgid "RDS Max Disconnection Time" msgstr "" -#: terminal/serializers/applet_host.py:34 +#: terminal/serializers/applet_host.py:36 msgid "RDS Remote App Logoff Time Limit" msgstr "" -#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +#: terminal/serializers/applet_host.py:42 terminal/serializers/terminal.py:41 msgid "Load status" msgstr "负载状态" #: terminal/serializers/endpoint.py:14 -msgid "Magnus listen db port" -msgstr "Magnus 监听的数据库端口" +msgid "Oracle port" +msgstr "Oracle 端口" #: terminal/serializers/endpoint.py:17 -msgid "Magnus Listen port range" -msgstr "Magnus 监听的端口范围" +msgid "Oracle port range" +msgstr "Oracle 端口范围" #: terminal/serializers/endpoint.py:19 msgid "" -"The range of ports that Magnus listens on is modified in the configuration " -"file" -msgstr "请在配置文件中修改 Magnus 监听的端口范围" +"Oracle proxy server listen port is dynamic, Each additional Oracle database " +"instance adds a port listener" +msgstr "" +"Oracle 代理服务器监听端口是动态的,每增加一个 Oracle 数据库实例,就会增加一个" +"端口监听" -#: terminal/serializers/endpoint.py:51 +#: terminal/serializers/endpoint.py:58 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" @@ -5352,43 +5630,39 @@ msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" msgid "Tunnel" msgstr "" -#: terminal/serializers/session.py:41 -msgid "User ID" -msgstr "用户 ID" - -#: terminal/serializers/session.py:42 -msgid "Asset ID" -msgstr "资产 ID" - -#: terminal/serializers/session.py:43 -msgid "Login from display" -msgstr "登录来源名称" - -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:28 terminal/serializers/session.py:50 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:46 +#: terminal/serializers/session.py:29 terminal/serializers/session.py:51 msgid "Can join" msgstr "是否可加入" -#: terminal/serializers/session.py:47 -msgid "Terminal ID" -msgstr "终端 ID" - -#: terminal/serializers/session.py:48 -msgid "Is finished" -msgstr "是否完成" - -#: terminal/serializers/session.py:49 +#: terminal/serializers/session.py:30 terminal/serializers/session.py:54 msgid "Can terminate" msgstr "是否可中断" -#: terminal/serializers/session.py:50 +#: terminal/serializers/session.py:46 +msgid "User ID" +msgstr "用户 ID" + +#: terminal/serializers/session.py:47 +msgid "Asset ID" +msgstr "资产 ID" + +#: terminal/serializers/session.py:48 +msgid "Login from display" +msgstr "登录来源名称" + +#: terminal/serializers/session.py:52 +msgid "Terminal ID" +msgstr "终端 ID" + +#: terminal/serializers/session.py:55 msgid "Terminal display" msgstr "终端显示" -#: terminal/serializers/session.py:55 +#: terminal/serializers/session.py:60 msgid "Command amount" msgstr "命令数量" @@ -5403,12 +5677,12 @@ msgstr "桶名称" #: terminal/serializers/storage.py:30 #: xpack/plugins/cloud/serializers/account_attrs.py:17 msgid "Access key id" -msgstr "访问密钥 ID(AK)" +msgstr "Access key ID(AK)" #: terminal/serializers/storage.py:34 #: xpack/plugins/cloud/serializers/account_attrs.py:20 msgid "Access key secret" -msgstr "访问密钥密文(SK)" +msgstr "Access key secret(SK)" #: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:217 msgid "Region" @@ -5466,7 +5740,7 @@ msgstr "没有发现" msgid "view" msgstr "查看" -#: terminal/utils/db_port_mapper.py:77 +#: terminal/utils/db_port_mapper.py:84 msgid "" "No available port is matched. The number of databases may have exceeded the " "number of ports open to the database agent service, Contact the " @@ -5475,13 +5749,13 @@ msgstr "" "未匹配到可用端口,数据库的数量可能已经超过数据库代理服务开放的端口数量,请联" "系管理员开放更多端口。" -#: terminal/utils/db_port_mapper.py:103 +#: terminal/utils/db_port_mapper.py:112 msgid "" "No ports can be used, check and modify the limit on the number of ports that " "Magnus listens on in the configuration file." msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听的端口数量限制。" -#: terminal/utils/db_port_mapper.py:105 +#: terminal/utils/db_port_mapper.py:114 msgid "All available port count: {}, Already use port count: {}" msgstr "所有可用端口数量:{},已使用端口数量:{}" @@ -5581,15 +5855,15 @@ msgid "Body" msgstr "内容" #: tickets/models/flow.py:19 tickets/models/flow.py:61 -#: tickets/models/ticket/general.py:40 +#: tickets/models/ticket/general.py:41 msgid "Approve level" msgstr "审批级别" -#: tickets/models/flow.py:24 tickets/serializers/flow.py:18 +#: tickets/models/flow.py:24 tickets/serializers/flow.py:17 msgid "Approve strategy" msgstr "审批策略" -#: tickets/models/flow.py:29 tickets/serializers/flow.py:20 +#: tickets/models/flow.py:29 tickets/serializers/flow.py:19 msgid "Assignees" msgstr "受理人" @@ -5623,20 +5897,14 @@ msgstr "申请的系统用户" msgid "Select at least one asset or node" msgstr "资产或者节点至少选择一项" -#: tickets/models/ticket/apply_asset.py:14 -#: tickets/serializers/ticket/apply_asset.py:26 -msgid "Apply nodes" -msgstr "申请节点" - -#: tickets/models/ticket/apply_asset.py:16 -#: tickets/serializers/ticket/apply_asset.py:22 -msgid "Apply assets" -msgstr "申请资产" - #: tickets/models/ticket/apply_asset.py:17 msgid "Apply accounts" msgstr "申请账号" +#: tickets/models/ticket/apply_asset.py:26 +msgid "Apply Asset Ticket" +msgstr "申请资产" + #: tickets/models/ticket/command_confirm.py:9 msgid "Run user" msgstr "运行的用户" @@ -5653,11 +5921,11 @@ msgstr "运行的命令" msgid "Command filter acl" msgstr "命令过滤器" -#: tickets/models/ticket/general.py:75 +#: tickets/models/ticket/general.py:76 msgid "Ticket step" msgstr "工单步骤" -#: tickets/models/ticket/general.py:93 +#: tickets/models/ticket/general.py:94 msgid "Ticket assignee" msgstr "工单受理人" @@ -5685,7 +5953,7 @@ msgstr "工单快照" msgid "Please try again" msgstr "请再次尝试" -#: tickets/models/ticket/general.py:425 +#: tickets/models/ticket/general.py:458 msgid "Super ticket" msgstr "超级工单" @@ -5729,19 +5997,19 @@ msgstr "你的工单已被处理, 处理人 - {}" msgid "Ticket has processed - {} ({})" msgstr "你的工单已被处理, 处理人 - {} ({})" -#: tickets/serializers/flow.py:21 +#: tickets/serializers/flow.py:20 msgid "Assignees display" msgstr "受理人名称" -#: tickets/serializers/flow.py:47 +#: tickets/serializers/flow.py:46 msgid "Please select the Assignees" msgstr "请选择受理人" -#: tickets/serializers/flow.py:75 +#: tickets/serializers/flow.py:74 msgid "The current organization type already exists" msgstr "当前组织已存在该类型" -#: tickets/serializers/super_ticket.py:11 +#: tickets/serializers/super_ticket.py:15 msgid "Processor" msgstr "处理人" @@ -5749,6 +6017,14 @@ msgstr "处理人" msgid "Support fuzzy search, and display up to 10 items" msgstr "支持模糊搜索,最多显示10项" +#: tickets/serializers/ticket/apply_asset.py:22 +msgid "Apply assets" +msgstr "申请资产" + +#: tickets/serializers/ticket/apply_asset.py:26 +msgid "Apply nodes" +msgstr "申请节点" + #: tickets/serializers/ticket/apply_asset.py:28 msgid "Apply actions" msgstr "申请动作" @@ -5766,7 +6042,7 @@ msgstr "过期时间要大于开始时间" msgid "Permission named `{}` already exists" msgstr "授权名称 `{}` 已存在" -#: tickets/serializers/ticket/ticket.py:95 +#: tickets/serializers/ticket/ticket.py:88 msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" @@ -5935,10 +6211,6 @@ msgstr "SSH公钥" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:631 -msgid "Local" -msgstr "数据库" - #: users/models/user.py:687 users/serializers/user.py:160 msgid "Is service account" msgstr "服务账号" @@ -5957,7 +6229,7 @@ msgstr "手机" #: users/models/user.py:701 msgid "OTP secret key" -msgstr "OTP 秘钥" +msgstr "OTP 密钥" #: users/models/user.py:705 msgid "Private key" @@ -5972,10 +6244,6 @@ msgstr "Secret key" msgid "Is first login" msgstr "首次登录" -#: users/models/user.py:727 -msgid "Source" -msgstr "来源" - #: users/models/user.py:731 msgid "Date password last updated" msgstr "最后更新密码日期" @@ -6780,7 +7048,7 @@ msgstr "证书文件" #: xpack/plugins/cloud/serializers/account_attrs.py:118 msgid "Key File" -msgstr "秘钥文件" +msgstr "密钥文件" #: xpack/plugins/cloud/serializers/account_attrs.py:134 msgid "Service account key" @@ -6897,26 +7165,94 @@ msgstr "许可证导入成功" msgid "License is invalid" msgstr "无效的许可证" -#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:127 +#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:135 msgid "License" msgstr "许可证" -#: xpack/plugins/license/models.py:71 +#: xpack/plugins/license/models.py:79 msgid "Standard edition" msgstr "标准版" -#: xpack/plugins/license/models.py:73 +#: xpack/plugins/license/models.py:81 msgid "Enterprise edition" msgstr "企业版" -#: xpack/plugins/license/models.py:75 +#: xpack/plugins/license/models.py:83 msgid "Ultimate edition" msgstr "旗舰版" -#: xpack/plugins/license/models.py:77 +#: xpack/plugins/license/models.py:85 msgid "Community edition" msgstr "社区版" +#~ msgid "Dynamic username" +#~ msgstr "同名账号" + +#~ msgid "Username already exists" +#~ msgstr "用户名 `{}` 已存在" + +#~ msgid "Dynamic username already exists" +#~ msgstr "动态用户名已存在" + +#~ msgid "Commands" +#~ msgstr "命令" + +#~ msgid "Change password enabled" +#~ msgstr "开启账号改密" + +#~ msgid "Change password method" +#~ msgstr "更改密码方式" + +#~ msgid "{} used account[{}], login method[{}] login the asset." +#~ msgstr "{} 使用账户[{}], 登录方式[{}]登录了这个资产." + +#~ msgid "User {} has executed change auth plan for this account.({})" +#~ msgstr "用户 {} 为这个账号执行了改密计划.({})" + +#~ msgid "Applet host" +#~ msgstr "远程应用发布机" + +#~ msgid "" +#~ "The range of ports that Magnus listens on is modified in the " +#~ "configuration file" +#~ msgstr "请在配置文件中修改 Magnus 监听的端口范围" + +#~ msgid "Magnus Listen port range" +#~ msgstr "Magnus 监听的端口范围" + +#~ msgid "Published" +#~ msgstr "已发布" + +#~ msgid "Unpublished" +#~ msgstr "未发布" + +#~ msgid "Not match" +#~ msgstr "没有匹配的" + +#~ msgid "Magnus listen db port" +#~ msgstr "Magnus 监听的数据库端口" + +#~ msgid "Update task content: {}" +#~ msgstr "更新任务内容: {}" + +#~ msgid "Execute batch command" +#~ msgstr "执行批量命令" + +#~ msgid "Create account" +#~ msgstr "创建账号" + +#~ msgid "Present" +#~ msgstr "存在" + +#~ msgid "Date last login" +#~ msgstr "最后登录日期" + +#~ msgid "IP last login" +#~ msgstr "最后登录IP" + +#~ msgid "GatherUser" +#~ msgstr "收集用户" + #~ msgid "Welcome back, please enter username and password to login" #~ msgstr "欢迎回来,请输入用户名和密码登录" @@ -7179,9 +7515,6 @@ msgstr "社区版" #~ msgid "Default Cluster" #~ msgstr "默认Cluster" -#~ msgid "Test gateway" -#~ msgstr "测试网关" - #~ msgid "User groups" #~ msgstr "用户组" @@ -7254,12 +7587,6 @@ msgstr "社区版" #~ msgid "Only system users with automatic login are allowed" #~ msgstr "仅允许自动登录的系统用户" -#~ msgid "System user name" -#~ msgstr "系统用户名称" - -#~ msgid "Asset hostname" -#~ msgstr "资产主机名" - #~ msgid "The asset {} system platform {} does not support run Ansible tasks" #~ msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" @@ -7358,9 +7685,6 @@ msgstr "社区版" #~ msgid "Callback" #~ msgstr "回调" -#~ msgid "Can view task monitor" -#~ msgstr "可以查看任务监控" - #~ msgid "Tasks" #~ msgstr "任务" diff --git a/apps/notifications/api/notifications.py b/apps/notifications/api/notifications.py index 5aa404896..bbac4b49a 100644 --- a/apps/notifications/api/notifications.py +++ b/apps/notifications/api/notifications.py @@ -2,7 +2,7 @@ from rest_framework.mixins import ListModelMixin, UpdateModelMixin, RetrieveMode from rest_framework.views import APIView from rest_framework.response import Response -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from common.permissions import IsValidUser from notifications.notifications import system_msgs from notifications.models import SystemMsgSubscription, UserMsgSubscription diff --git a/apps/notifications/api/site_msgs.py b/apps/notifications/api/site_msgs.py index 29bd785d5..5c9257c25 100644 --- a/apps/notifications/api/site_msgs.py +++ b/apps/notifications/api/site_msgs.py @@ -2,10 +2,10 @@ from rest_framework.response import Response from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.decorators import action -from common.http import is_true +from common.utils.http import is_true from common.permissions import IsValidUser from common.const.http import GET, PATCH, POST -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from ..serializers import ( SiteMessageDetailSerializer, SiteMessageIdsSerializer, SiteMessageSendSerializer, @@ -13,7 +13,7 @@ from ..serializers import ( from ..site_msg import SiteMessageUtil from ..filters import SiteMsgFilter -__all__ = ('SiteMessageViewSet', ) +__all__ = ('SiteMessageViewSet',) class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JMSGenericViewSet): diff --git a/apps/notifications/serializers/notifications.py b/apps/notifications/serializers/notifications.py index fcf8c811c..04ba2560b 100644 --- a/apps/notifications/serializers/notifications.py +++ b/apps/notifications/serializers/notifications.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from common.drf.serializers import BulkModelSerializer +from common.serializers import BulkModelSerializer from notifications.models import SystemMsgSubscription, UserMsgSubscription diff --git a/apps/ops/ansible/ansible.cfg b/apps/ops/ansible/ansible.cfg new file mode 100644 index 000000000..c51b2d733 --- /dev/null +++ b/apps/ops/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +forks = 10 +host_key_checking = False +library = /opt/jumpserver/apps/ops/ansible/modules:./modules +[inventory] +[privilege_escalation] +[paramiko_connection] +[ssh_connection] +[persistent_connection] +[accelerate] +[selinux] +[colors] +[diff] diff --git a/apps/ops/ansible/modules_utils/oracle_common.py b/apps/ops/ansible/modules_utils/oracle_common.py index c88f04373..8036a8c1e 100644 --- a/apps/ops/ansible/modules_utils/oracle_common.py +++ b/apps/ops/ansible/modules_utils/oracle_common.py @@ -56,7 +56,7 @@ class OracleClient(object): def cursor(self): if self._cursor is None: try: - oracledb.init_oracle_client(lib_dir='/Users/jiangweidong/Downloads/instantclient_19_8') + oracledb.init_oracle_client(lib_dir='/opt/oracle/instantclient_19_10') self._conn = oracledb.connect(**self.connect_params) self._cursor = self._conn.cursor() except DatabaseError as err: diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 280f8f05c..18df1a702 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -1,5 +1,5 @@ -import uuid import os +import uuid import ansible_runner from django.conf import settings @@ -78,6 +78,7 @@ class PlaybookRunner: verbosity=verbosity, event_handler=self.cb.event_handler, status_handler=self.cb.status_handler, + host_cwd=self.project_dir, **kwargs ) return self.cb diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 8d58c1981..f55c296f9 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -17,7 +17,7 @@ from ..models import CeleryTaskExecution, CeleryTask from ..serializers import CeleryResultSerializer, CeleryPeriodTaskSerializer from ..celery.utils import get_celery_task_log_path from ..ansible.utils import get_ansible_task_log_path -from common.mixins.api import CommonApiMixin +from common.api import CommonApiMixin __all__ = [ 'CeleryTaskExecutionLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet', diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index 817e5040e..30bd0ee18 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -13,7 +13,7 @@ from ops.tasks import run_ops_job_execution from ops.variables import JMS_JOB_VARIABLE_HELP from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_org, get_current_org_id, get_current_org -from assets.models import Account +from accounts.models import Account def set_task_to_serializer_data(serializer, task): @@ -105,7 +105,7 @@ class JobExecutionTaskDetail(APIView): def get(self, request, **kwargs): org = get_current_org() - task_id = request.query_params.get('task_id') + task_id = str(kwargs.get('task_id')) if task_id: with tmp_to_org(org): execution = get_object_or_404(JobExecution, task_id=task_id) @@ -114,6 +114,7 @@ class JobExecutionTaskDetail(APIView): 'is_finished': execution.is_finished, 'is_success': execution.is_success, 'time_cost': execution.time_cost, + 'job_id': execution.job.id, }) @@ -122,5 +123,6 @@ class FrequentUsernames(APIView): permission_classes = () def get(self, request, **kwargs): - top_accounts = Account.objects.all().values('username').annotate(total=Count('username')).order_by('total') + top_accounts = Account.objects.exclude(username='root').exclude(username__startswith='jms_').values('username').annotate( + total=Count('username')).order_by('total')[:5] return Response(data=top_accounts) diff --git a/apps/ops/celery/logger.py b/apps/ops/celery/logger.py index 46b7e626c..4c1462583 100644 --- a/apps/ops/celery/logger.py +++ b/apps/ops/celery/logger.py @@ -1,13 +1,14 @@ from logging import StreamHandler from threading import get_ident -from django.conf import settings from celery import current_task from celery.signals import task_prerun, task_postrun +from django.conf import settings from kombu import Connection, Exchange, Queue, Producer from kombu.mixins import ConsumerMixin from .utils import get_celery_task_log_path +from ..const import CELERY_LOG_MAGIC_MARK routing_key = 'celery_log' celery_log_exchange = Exchange('celery_log_exchange', type='direct') @@ -198,8 +199,8 @@ class CeleryThreadTaskFileHandler(CeleryThreadingLoggerHandler): if not f: raise ValueError('Not found thread task file') msg = self.format(record) - f.write(msg) - f.write(self.terminator) + f.write(msg.encode()) + f.write(self.terminator.encode()) f.flush() def flush(self): @@ -210,12 +211,13 @@ class CeleryThreadTaskFileHandler(CeleryThreadingLoggerHandler): log_path = get_celery_task_log_path(task_id) thread_id = self.get_current_thread_id() self.task_id_thread_id_mapper[task_id] = thread_id - f = open(log_path, 'a') + f = open(log_path, 'ab') self.thread_id_fd_mapper[thread_id] = f def handle_task_end(self, task_id): ident_id = self.task_id_thread_id_mapper.get(task_id, '') f = self.thread_id_fd_mapper.pop(ident_id, None) if f and not f.closed: + f.write(CELERY_LOG_MAGIC_MARK) f.close() self.task_id_thread_id_mapper.pop(task_id, None) diff --git a/apps/ops/const.py b/apps/ops/const.py index c383ef3c7..8838c31c5 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -51,3 +51,7 @@ class JobStatus(models.TextChoices): success = 'success', _('Success') timeout = 'timeout', _('Timeout') failed = 'failed', _('Failed') + + +# celery 日志完成之后,写入的魔法字符,作为结束标记 +CELERY_LOG_MAGIC_MARK = b'\x00\x00\x00\x00\x00' diff --git a/apps/ops/migrations/0022_auto_20220817_1346.py b/apps/ops/migrations/0022_auto_20220817_1346.py index b06c85a18..2e1c8be47 100644 --- a/apps/ops/migrations/0022_auto_20220817_1346.py +++ b/apps/ops/migrations/0022_auto_20220817_1346.py @@ -3,45 +3,51 @@ from django.db import migrations, models -def migrate_run_system_user_to_account(apps, schema_editor): - execution_model = apps.get_model('ops', 'CommandExecution') - count = 0 - bulk_size = 1000 - - while True: - executions = execution_model.objects.all().prefetch_related('run_as')[count:bulk_size] - if not executions: - break - count += len(executions) - updated = [] - for obj in executions: - run_as = obj.run_as - if not run_as: - continue - obj.account = run_as.username - updated.append(obj) - execution_model.objects.bulk_update(updated, ['account']) - - class Migration(migrations.Migration): - dependencies = [ ('ops', '0021_auto_20211130_1037'), ] operations = [ migrations.RemoveField( - model_name='adhoc', - name='run_system_user', + model_name='adhocexecution', + name='adhoc', ), - migrations.AddField( - model_name='commandexecution', - name='account', - field=models.CharField(default='', max_length=128, verbose_name='account'), + migrations.RemoveField( + model_name='adhocexecution', + name='task', ), - migrations.RunPython(migrate_run_system_user_to_account), migrations.RemoveField( model_name='commandexecution', - name='run_as', + name='hosts', ), + migrations.RemoveField( + model_name='commandexecution', + name='user', + ), + migrations.AlterUniqueTogether( + name='task', + unique_together=None, + ), + migrations.RemoveField( + model_name='task', + name='latest_adhoc', + ), + migrations.RemoveField( + model_name='task', + name='latest_execution', + ), + migrations.DeleteModel( + name='AdHoc', + ), + migrations.DeleteModel( + name='AdHocExecution', + ), + migrations.DeleteModel( + name='CommandExecution', + ), + migrations.DeleteModel( + name='Task', + ), + migrations.DeleteModel('CeleryTask'), ] diff --git a/apps/ops/migrations/0023_auto_20220912_0021.py b/apps/ops/migrations/0023_auto_20220912_0021.py new file mode 100644 index 000000000..edf8b0eeb --- /dev/null +++ b/apps/ops/migrations/0023_auto_20220912_0021.py @@ -0,0 +1,245 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ('assets', '0105_auto_20221220_1956'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0022_auto_20220817_1346'), + ] + + operations = [ + migrations.CreateModel( + name='CeleryTask', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=1024, verbose_name='Name')), + ('date_last_publish', models.DateTimeField(null=True, verbose_name='Date last publish')), + ], + options={ + 'verbose_name': 'Celery Task', + 'ordering': ('name',), + 'permissions': [('view_taskmonitor', 'Can view task monitor')], + }, + ), + migrations.CreateModel( + name='CeleryTaskExecution', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=1024)), + ('args', models.JSONField(verbose_name='Args')), + ('kwargs', models.JSONField(verbose_name='Kwargs')), + ('state', models.CharField(max_length=16, verbose_name='State')), + ('is_finished', models.BooleanField(default=False, verbose_name='Finished')), + ('date_published', models.DateTimeField(auto_now_add=True, verbose_name='Date published')), + ('date_start', models.DateTimeField(null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ], + options={ + 'verbose_name': 'Celery Task Execution', + }, + ), + migrations.CreateModel( + name='Job', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('instant', models.BooleanField(default=False)), + ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), + ('module', + models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], + default='shell', max_length=128, null=True, verbose_name='Module')), + ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), + ('timeout', models.IntegerField(default=-1, verbose_name='Timeout (Seconds)')), + ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', + max_length=128, verbose_name='Type')), + ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), + ('runas_policy', models.CharField( + choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), + ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), + ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), + ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('version', models.IntegerField(default=0)), + ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'verbose_name': 'Job', + 'ordering': ['date_created'], + }, + ), + migrations.CreateModel( + name='Playbook', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('path', models.FileField(upload_to='playbooks/')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='JobExecution', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('task_id', models.UUIDField(null=True)), + ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), + ('job_version', models.IntegerField(default=0)), + ('parameters', models.JSONField(default=dict, verbose_name='Parameters')), + ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), + ('summary', models.JSONField(default=dict, verbose_name='Summary')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ('job', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', + to='ops.job')), + ], + options={ + 'verbose_name': 'Job Execution', + 'ordering': ['-date_created'], + }, + ), + migrations.AddField( + model_name='job', + name='playbook', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbook', + verbose_name='Playbook'), + ), + migrations.CreateModel( + name='HistoricalJob', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', + models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('instant', models.BooleanField(default=False)), + ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), + ('module', + models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], + default='shell', max_length=128, null=True, verbose_name='Module')), + ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), + ('timeout', models.IntegerField(default=-1, verbose_name='Timeout (Seconds)')), + ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', + max_length=128, verbose_name='Type')), + ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), + ('runas_policy', models.CharField( + choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), + ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), + ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), + ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('version', models.IntegerField(default=0)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', + models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('creator', models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ('history_user', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', + to=settings.AUTH_USER_MODEL)), + ('playbook', models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to='ops.playbook', verbose_name='Playbook')), + ], + options={ + 'verbose_name': 'historical Job', + 'verbose_name_plural': 'historical Jobs', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='AdHoc', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('pattern', models.CharField(default='all', max_length=1024, verbose_name='Pattern')), + ('module', + models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], + default='shell', max_length=128, verbose_name='Module')), + ('args', models.CharField(default='', max_length=1024, verbose_name='Args')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'verbose_name': 'AdHoc', + }, + ), + migrations.CreateModel( + name='JobAuditLog', + fields=[ + ], + options={ + 'verbose_name': 'Job audit log', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('ops.jobexecution',), + ), + ] diff --git a/apps/ops/migrations/0023_auto_20220929_2025.py b/apps/ops/migrations/0023_auto_20220929_2025.py deleted file mode 100644 index b5c7475f4..000000000 --- a/apps/ops/migrations/0023_auto_20220929_2025.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.2.14 on 2022-09-29 12:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0022_auto_20220817_1346'), - ] - - operations = [ - migrations.RemoveField( - model_name='celerytask', - name='log_path', - ), - migrations.RemoveField( - model_name='celerytask', - name='status', - ), - migrations.AddField( - model_name='celerytask', - name='args', - field=models.JSONField(default=[], verbose_name='Args'), - preserve_default=False, - ), - migrations.AddField( - model_name='celerytask', - name='is_finished', - field=models.BooleanField(default=False, verbose_name='Finished'), - ), - migrations.AddField( - model_name='celerytask', - name='kwargs', - field=models.JSONField(default={}, verbose_name='Kwargs'), - preserve_default=False, - ), - migrations.AddField( - model_name='celerytask', - name='state', - field=models.CharField(default='SUCCESS', max_length=16, verbose_name='State'), - preserve_default=False, - ), - ] diff --git a/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py b/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py new file mode 100644 index 000000000..9c09af231 --- /dev/null +++ b/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2022-12-30 08:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0023_auto_20220912_0021'), + ] + + operations = [ + migrations.AlterField( + model_name='celerytask', + name='date_last_publish', + field=models.DateTimeField(null=True, verbose_name='Date last publish'), + ), + migrations.AlterField( + model_name='celerytaskexecution', + name='name', + field=models.CharField(max_length=1024, verbose_name='Name'), + ), + ] diff --git a/apps/ops/migrations/0024_auto_20221008_1514.py b/apps/ops/migrations/0024_auto_20221008_1514.py deleted file mode 100644 index e208af96e..000000000 --- a/apps/ops/migrations/0024_auto_20221008_1514.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-08 07:19 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0106_auto_20220916_1556'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ops', '0023_auto_20220929_2025'), - ] - - operations = [ - migrations.RemoveField( - model_name='adhocexecution', - name='adhoc', - ), - migrations.RemoveField( - model_name='adhocexecution', - name='task', - ), - migrations.RemoveField( - model_name='commandexecution', - name='hosts', - ), - migrations.RemoveField( - model_name='commandexecution', - name='user', - ), - migrations.AlterUniqueTogether( - name='task', - unique_together=None, - ), - migrations.RemoveField( - model_name='task', - name='latest_adhoc', - ), - migrations.RemoveField( - model_name='task', - name='latest_execution', - ), - migrations.DeleteModel( - name='AdHoc', - ), - migrations.DeleteModel( - name='AdHocExecution', - ), - migrations.DeleteModel( - name='CommandExecution', - ), - migrations.DeleteModel( - name='Task', - ), - ] diff --git a/apps/ops/migrations/0025_auto_20221008_1631.py b/apps/ops/migrations/0025_auto_20221008_1631.py deleted file mode 100644 index 7e814c3d1..000000000 --- a/apps/ops/migrations/0025_auto_20221008_1631.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-08 08:31 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0106_auto_20220916_1556'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ops', '0024_auto_20221008_1514'), - ] - - operations = [ - migrations.CreateModel( - name='AdHoc', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False)), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('account', models.CharField(default='root', max_length=128, verbose_name='Account')), - ('account_policy', models.CharField(default='root', max_length=128, verbose_name='Account policy')), - ('date_last_run', models.DateTimeField(null=True, verbose_name='Date last run')), - ('pattern', models.CharField(default='all', max_length=1024, verbose_name='Pattern')), - ('module', models.CharField(default='shell', max_length=128, verbose_name='Module')), - ('args', models.CharField(default='', max_length=1024, verbose_name='Args')), - ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='AdHocExecution', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), - ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), - ('summary', models.JSONField(default=dict, verbose_name='Summary')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True)), - ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='ops.adhoc', verbose_name='Adhoc')), - ], - options={ - 'verbose_name': 'AdHoc execution', - 'db_table': 'ops_adhoc_execution', - 'get_latest_by': 'date_start', - }, - ), - migrations.AddField( - model_name='adhoc', - name='last_execution', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.adhocexecution', verbose_name='Last execution'), - ), - migrations.AddField( - model_name='adhoc', - name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'), - ), - ] diff --git a/apps/ops/migrations/0026_auto_20221009_2050.py b/apps/ops/migrations/0026_auto_20221009_2050.py deleted file mode 100644 index b9965c2bd..000000000 --- a/apps/ops/migrations/0026_auto_20221009_2050.py +++ /dev/null @@ -1,108 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-09 12:50 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('assets', '0106_auto_20220916_1556'), - ('ops', '0025_auto_20221008_1631'), - ] - - operations = [ - migrations.CreateModel( - name='Playbook', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False)), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('account', models.CharField(default='root', max_length=128, verbose_name='Account')), - ('account_policy', models.CharField(default='root', max_length=128, verbose_name='Account policy')), - ('date_last_run', models.DateTimeField(null=True, verbose_name='Date last run')), - ('path', models.FilePathField(max_length=1024, verbose_name='Playbook')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AlterField( - model_name='adhocexecution', - name='date_finished', - field=models.DateTimeField(null=True, verbose_name='Date finished'), - ), - migrations.CreateModel( - name='PlaybookTemplate', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('path', models.FilePathField(verbose_name='Path')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ], - options={ - 'verbose_name': 'Playbook template', - 'ordering': ['name'], - 'unique_together': {('org_id', 'name')}, - }, - ), - migrations.CreateModel( - name='PlaybookExecution', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), - ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), - ('summary', models.JSONField(default=dict, verbose_name='Summary')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ('path', models.FilePathField(max_length=1024, verbose_name='Run dir')), - ('creator', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, - verbose_name='Creator')), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ops.playbook', - verbose_name='Task')), - ], - options={ - 'ordering': ['-date_start'], - 'abstract': False, - }, - ), - migrations.AddField( - model_name='playbook', - name='last_execution', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to='ops.playbookexecution', verbose_name='Last execution'), - ), - migrations.AddField( - model_name='playbook', - name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, verbose_name='Owner'), - ), - migrations.AddField( - model_name='playbook', - name='template', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbooktemplate', - verbose_name='Template'), - ), - ] diff --git a/apps/ops/migrations/0027_auto_20221024_1709.py b/apps/ops/migrations/0027_auto_20221024_1709.py deleted file mode 100644 index 2ded6ca58..000000000 --- a/apps/ops/migrations/0027_auto_20221024_1709.py +++ /dev/null @@ -1,273 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 03:23 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('assets', '0112_gateway_to_asset'), - ('ops', '0026_auto_20221009_2050'), - ] - - operations = [ - migrations.CreateModel( - name='CeleryTaskExecution', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=1024)), - ('args', models.JSONField(verbose_name='Args')), - ('kwargs', models.JSONField(verbose_name='Kwargs')), - ('state', models.CharField(max_length=16, verbose_name='State')), - ('is_finished', models.BooleanField(default=False, verbose_name='Finished')), - ('date_published', models.DateTimeField(auto_now_add=True, verbose_name='Date published')), - ('date_start', models.DateTimeField(null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ], - ), - migrations.CreateModel( - name='Job', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), - ('instant', models.BooleanField(default=False)), - ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), - ('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', - max_length=128, null=True, verbose_name='Module')), - ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), - ('timeout', models.IntegerField(default=60, verbose_name='Timeout (Seconds)')), - ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', - max_length=128, verbose_name='Type')), - ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), - ('runas_policy', models.CharField( - choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), - ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), - ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), - ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), - ('comment', - models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), - ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), - ('owner', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, - verbose_name='Creator')), - ], - options={ - 'ordering': ['date_created'], - }, - ), - migrations.CreateModel( - name='JobExecution', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('task_id', models.UUIDField(null=True)), - ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), - ('parameters', models.JSONField(default=dict, verbose_name='Parameters')), - ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), - ('summary', models.JSONField(default=dict, verbose_name='Summary')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ('creator', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, - verbose_name='Creator')), - ('job', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', - to='ops.job')), - ], - options={ - 'ordering': ['-date_created'], - }, - ), - migrations.RemoveField( - model_name='playbookexecution', - name='creator', - ), - migrations.RemoveField( - model_name='playbookexecution', - name='task', - ), - migrations.AlterUniqueTogether( - name='playbooktemplate', - unique_together=None, - ), - migrations.AlterModelOptions( - name='celerytask', - options={'ordering': ('name',)}, - ), - migrations.RenameField( - model_name='adhoc', - old_name='owner', - new_name='creator', - ), - migrations.RenameField( - model_name='celerytask', - old_name='date_finished', - new_name='last_published_time', - ), - migrations.RemoveField( - model_name='adhoc', - name='account', - ), - migrations.RemoveField( - model_name='adhoc', - name='account_policy', - ), - migrations.RemoveField( - model_name='adhoc', - name='assets', - ), - migrations.RemoveField( - model_name='adhoc', - name='crontab', - ), - migrations.RemoveField( - model_name='adhoc', - name='date_last_run', - ), - migrations.RemoveField( - model_name='adhoc', - name='interval', - ), - migrations.RemoveField( - model_name='adhoc', - name='is_periodic', - ), - migrations.RemoveField( - model_name='adhoc', - name='last_execution', - ), - migrations.RemoveField( - model_name='celerytask', - name='args', - ), - migrations.RemoveField( - model_name='celerytask', - name='date_published', - ), - migrations.RemoveField( - model_name='celerytask', - name='date_start', - ), - migrations.RemoveField( - model_name='celerytask', - name='is_finished', - ), - migrations.RemoveField( - model_name='celerytask', - name='kwargs', - ), - migrations.RemoveField( - model_name='celerytask', - name='state', - ), - migrations.RemoveField( - model_name='playbook', - name='account', - ), - migrations.RemoveField( - model_name='playbook', - name='account_policy', - ), - migrations.RemoveField( - model_name='playbook', - name='assets', - ), - migrations.RemoveField( - model_name='playbook', - name='crontab', - ), - migrations.RemoveField( - model_name='playbook', - name='date_last_run', - ), - migrations.RemoveField( - model_name='playbook', - name='interval', - ), - migrations.RemoveField( - model_name='playbook', - name='is_periodic', - ), - migrations.RemoveField( - model_name='playbook', - name='last_execution', - ), - migrations.RemoveField( - model_name='playbook', - name='owner', - ), - migrations.RemoveField( - model_name='playbook', - name='template', - ), - migrations.AddField( - model_name='adhoc', - name='comment', - field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), - ), - migrations.AddField( - model_name='playbook', - name='creator', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, verbose_name='Creator'), - ), - migrations.AlterField( - model_name='adhoc', - name='module', - field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', - max_length=128, verbose_name='Module'), - ), - migrations.AlterField( - model_name='celerytask', - name='name', - field=models.CharField(max_length=1024, verbose_name='Name'), - ), - migrations.AlterField( - model_name='playbook', - name='comment', - field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), - ), - migrations.AlterField( - model_name='playbook', - name='name', - field=models.CharField(max_length=128, null=True, verbose_name='Name'), - ), - migrations.AlterField( - model_name='playbook', - name='path', - field=models.FileField(upload_to='playbooks/'), - ), - migrations.DeleteModel( - name='AdHocExecution', - ), - migrations.DeleteModel( - name='PlaybookExecution', - ), - migrations.DeleteModel( - name='PlaybookTemplate', - ), - migrations.AddField( - model_name='job', - name='playbook', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbook', - verbose_name='Playbook'), - ), - ] diff --git a/apps/ops/migrations/0028_auto_20221205_1627.py b/apps/ops/migrations/0028_auto_20221205_1627.py deleted file mode 100644 index 5b04102a7..000000000 --- a/apps/ops/migrations/0028_auto_20221205_1627.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 08:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0027_auto_20221024_1709'), - ] - - operations = [ - migrations.RenameField( - model_name='job', - old_name='owner', - new_name='creator', - ), - migrations.RemoveField( - model_name='adhoc', - name='org_id', - ), - migrations.RemoveField( - model_name='job', - name='org_id', - ), - migrations.RemoveField( - model_name='jobexecution', - name='org_id', - ), - migrations.RemoveField( - model_name='playbook', - name='org_id', - ), - ] diff --git a/apps/ops/migrations/0029_auto_20221215_1712.py b/apps/ops/migrations/0029_auto_20221215_1712.py deleted file mode 100644 index b7dc3ea6d..000000000 --- a/apps/ops/migrations/0029_auto_20221215_1712.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-15 09:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0028_auto_20221205_1627'), - ] - - operations = [ - migrations.AddField( - model_name='adhoc', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AddField( - model_name='job', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AddField( - model_name='jobexecution', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AddField( - model_name='playbook', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - ] diff --git a/apps/ops/migrations/0030_auto_20221220_1941.py b/apps/ops/migrations/0030_auto_20221220_1941.py deleted file mode 100644 index cebbd8aa1..000000000 --- a/apps/ops/migrations/0030_auto_20221220_1941.py +++ /dev/null @@ -1,80 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-20 11:41 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import simple_history.models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ops', '0029_auto_20221215_1712'), - ] - - operations = [ - migrations.CreateModel( - name='JobAuditLog', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=('ops.jobexecution',), - ), - migrations.AddField( - model_name='job', - name='version', - field=models.IntegerField(default=0), - ), - migrations.AddField( - model_name='jobexecution', - name='job_version', - field=models.IntegerField(default=0), - ), - migrations.CreateModel( - name='HistoricalJob', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), - ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), - ('instant', models.BooleanField(default=False)), - ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), - ('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', max_length=128, null=True, verbose_name='Module')), - ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), - ('timeout', models.IntegerField(default=60, verbose_name='Timeout (Seconds)')), - ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', max_length=128, verbose_name='Type')), - ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), - ('runas_policy', models.CharField(choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), - ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), - ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), - ('comment', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), - ('version', models.IntegerField(default=0)), - ('history_id', models.AutoField(primary_key=True, serialize=False)), - ('history_date', models.DateTimeField(db_index=True)), - ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('creator', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')), - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ('playbook', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ops.playbook', verbose_name='Playbook')), - ], - options={ - 'verbose_name': 'historical job', - 'verbose_name_plural': 'historical jobs', - 'ordering': ('-history_date', '-history_id'), - 'get_latest_by': ('history_date', 'history_id'), - }, - bases=(simple_history.models.HistoricalChanges, models.Model), - ), - ] diff --git a/apps/ops/migrations/0031_auto_20221220_1956.py b/apps/ops/migrations/0031_auto_20221220_1956.py deleted file mode 100644 index 9cbc21547..000000000 --- a/apps/ops/migrations/0031_auto_20221220_1956.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-20 11:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('ops', '0030_auto_20221220_1941'), - ] - - operations = [ - migrations.AddField( - model_name='jobexecution', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), - migrations.AlterField( - model_name='adhoc', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='adhoc', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='job', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='job', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='jobexecution', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='jobexecution', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='playbook', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='playbook', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - ] diff --git a/apps/ops/migrations/0032_auto_20221221_1513.py b/apps/ops/migrations/0032_auto_20221221_1513.py deleted file mode 100644 index 3a706f4b7..000000000 --- a/apps/ops/migrations/0032_auto_20221221_1513.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-21 07:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0031_auto_20221220_1956'), - ] - - operations = [ - migrations.AlterField( - model_name='historicaljob', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='historicaljob', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - ] diff --git a/apps/ops/migrations/0033_auto_20221223_1536.py b/apps/ops/migrations/0033_auto_20221223_1536.py deleted file mode 100644 index f46955209..000000000 --- a/apps/ops/migrations/0033_auto_20221223_1536.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-23 07:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0032_auto_20221221_1513'), - ] - - operations = [ - migrations.AlterModelOptions( - name='adhoc', - options={'verbose_name': 'AdHoc'}, - ), - migrations.AlterModelOptions( - name='celerytask', - options={'ordering': ('name',), 'verbose_name': 'Celery Task'}, - ), - migrations.AlterModelOptions( - name='celerytaskexecution', - options={'verbose_name': 'Celery Task Execution'}, - ), - migrations.AlterModelOptions( - name='historicaljob', - options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Job', 'verbose_name_plural': 'historical Jobs'}, - ), - migrations.AlterModelOptions( - name='job', - options={'ordering': ['date_created'], 'verbose_name': 'Job'}, - ), - migrations.AlterModelOptions( - name='jobauditlog', - options={'verbose_name': 'Job audit log'}, - ), - migrations.AlterModelOptions( - name='jobexecution', - options={'ordering': ['-date_created'], 'verbose_name': 'Job Execution'}, - ), - ] diff --git a/apps/ops/migrations/0034_alter_celerytask_options.py b/apps/ops/migrations/0034_alter_celerytask_options.py deleted file mode 100644 index 9645782c4..000000000 --- a/apps/ops/migrations/0034_alter_celerytask_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-27 06:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0033_auto_20221223_1536'), - ] - - operations = [ - migrations.AlterModelOptions( - name='celerytask', - options={'ordering': ('name',), 'permissions': [('view_taskmonitor', 'Can view task monitor')], 'verbose_name': 'Celery Task'}, - ), - ] diff --git a/apps/ops/migrations/0035_auto_20221227_1520.py b/apps/ops/migrations/0035_auto_20221227_1520.py deleted file mode 100644 index 36752a9bb..000000000 --- a/apps/ops/migrations/0035_auto_20221227_1520.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-27 07:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0034_alter_celerytask_options'), - ] - - operations = [ - migrations.AlterField( - model_name='historicaljob', - name='module', - field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], default='shell', max_length=128, null=True, verbose_name='Module'), - ), - migrations.AlterField( - model_name='historicaljob', - name='timeout', - field=models.IntegerField(default=-1, verbose_name='Timeout (Seconds)'), - ), - migrations.AlterField( - model_name='job', - name='module', - field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], default='shell', max_length=128, null=True, verbose_name='Module'), - ), - migrations.AlterField( - model_name='job', - name='timeout', - field=models.IntegerField(default=-1, verbose_name='Timeout (Seconds)'), - ), - ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 4e8219ee1..b6c84183d 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -9,15 +9,14 @@ from common.utils import get_logger __all__ = ["AdHoc"] +from ops.const import Modules + from orgs.mixins.models import JMSOrgBaseModel logger = get_logger(__file__) class AdHoc(JMSOrgBaseModel): - class Modules(models.TextChoices): - shell = 'shell', _('Shell') - winshell = 'win_shell', _('Powershell') id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) diff --git a/apps/ops/models/celery.py b/apps/ops/models/celery.py index 316eca876..e0f566ff1 100644 --- a/apps/ops/models/celery.py +++ b/apps/ops/models/celery.py @@ -13,7 +13,7 @@ from ops.celery import app class CeleryTask(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) name = models.CharField(max_length=1024, verbose_name=_('Name')) - last_published_time = models.DateTimeField(null=True) + date_last_publish = models.DateTimeField(null=True, verbose_name=_("Date last publish")) @property def meta(self): @@ -54,7 +54,7 @@ class CeleryTask(models.Model): class CeleryTaskExecution(models.Model): LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery') id = models.UUIDField(primary_key=True, default=uuid.uuid4) - name = models.CharField(max_length=1024) + name = models.CharField(max_length=1024, verbose_name=_('Name')) args = models.JSONField(verbose_name=_("Args")) kwargs = models.JSONField(verbose_name=_("Kwargs")) state = models.CharField(max_length=16, verbose_name=_("State")) diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 85f960df1..894ff47d1 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -1,3 +1,4 @@ +import datetime import json import logging import os @@ -159,17 +160,29 @@ class JobExecution(JMSOrgBaseModel): @property def job_type(self): - return self.current_job.type + return Types[self.job.type].label def compile_shell(self): if self.current_job.type != 'adhoc': return - result = self.current_job.args - if self.current_job.chdir: - result += " chdir={}".format(self.current_job.chdir) + + module = self.current_job.module + # replace win_shell + if module == 'win_shell': + module = 'ansible.windows.win_shell' + if self.current_job.module in ['python']: - result += " executable={}".format(self.current_job.module) - return result + module = "shell" + + shell = self.current_job.args + if self.current_job.chdir: + if module == self.current_job.module: + shell += " path={}".format(self.current_job.chdir) + else: + shell += " chdir={}".format(self.current_job.chdir) + if self.current_job.module in ['python']: + shell += " executable={}".format(self.current_job.module) + return module, shell def get_runner(self): inv = self.current_job.inventory @@ -189,10 +202,8 @@ class JobExecution(JMSOrgBaseModel): extra_vars.update(static_variables) if self.current_job.type == 'adhoc': - args = self.compile_shell() - module = "shell" - if self.current_job.module not in ['python']: - module = self.current_job.module + + module, args = self.compile_shell() runner = AdHocRunner( self.inventory_path, @@ -226,9 +237,9 @@ class JobExecution(JMSOrgBaseModel): @property def time_cost(self): - if self.date_finished and self.date_start: + if self.is_finished: return (self.date_finished - self.date_start).total_seconds() - return None + return (timezone.now() - self.date_start).total_seconds() @property def timedelta(self): diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 7db49fdcd..58cbaad98 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -3,17 +3,15 @@ from __future__ import unicode_literals from rest_framework import serializers -from common.drf.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AdHoc class AdHocSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) - row_count = serializers.IntegerField(read_only=True) - size = serializers.IntegerField(read_only=True) class Meta: model = AdHoc - read_only_field = ["id", "row_count", "size", "creator", "date_created", "date_updated"] + read_only_field = ["id", "creator", "date_created", "date_updated"] fields = read_only_field + ["id", "name", "module", "args", "comment"] diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py index 776b1fd5b..12efdbea8 100644 --- a/apps/ops/serializers/celery.py +++ b/apps/ops/serializers/celery.py @@ -31,7 +31,7 @@ class CeleryPeriodTaskSerializer(serializers.ModelSerializer): class CeleryTaskSerializer(serializers.ModelSerializer): class Meta: model = CeleryTask - read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'last_published_time'] + read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'date_last_publish'] fields = read_only_fields diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 4b6ea82d1..f5e4ee5d6 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -1,11 +1,11 @@ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from assets.models import Node -from common.drf.fields import ReadableHiddenField +from perms.utils.user_perm import UserPermAssetUtil +from common.serializers.fields import ReadableHiddenField from ops.mixin import PeriodTaskSerializerMixin from ops.models import Job, JobExecution -from ops.models.job import JobAuditLog from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -13,42 +13,54 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) run_after_save = serializers.BooleanField(label=_("Run after save"), default=False, required=False) nodes = serializers.ListField(required=False, child=serializers.CharField()) + date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True) + + def get_request_user(self): + request = self.context.get('request') + user = request.user if request else None + return user def create(self, validated_data): assets = validated_data.__getitem__('assets') - node_ids = validated_data.pop('nodes') + node_ids = validated_data.pop('nodes', None) if node_ids: - nodes = Node.objects.filter(id__in=node_ids) - assets.extend( - Node.get_nodes_all_assets(*nodes).exclude(id__in=[asset.id for asset in assets])) + user = self.get_request_user() + perm_util = UserPermAssetUtil(user=user) + for node_id in node_ids: + node, node_assets = perm_util.get_node_all_assets(node_id) + assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets])) return super().create(validated_data) class Meta: model = Job - read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"] + read_only_fields = [ + "id", "date_last_run", "date_created", "date_updated", "average_time_cost" + ] fields = read_only_fields + [ - "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "creator", - "use_parameter_define", - "parameters_define", - "timeout", - "chdir", - "comment", - "summary", - "is_periodic", "interval", "crontab", "run_after_save", "nodes" + "name", "instant", "type", "module", + "args", "playbook", "assets", + "runas_policy", "runas", "creator", + "use_parameter_define", "parameters_define", + "timeout", "chdir", "comment", "summary", + "is_periodic", "interval", "crontab", "nodes", + "run_after_save", ] class JobExecutionSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) job_type = serializers.ReadOnlyField(label=_("Job type")) - material = serializers.ReadOnlyField(label=_("Material")) + material = serializers.ReadOnlyField(label=_("Command")) + is_success = serializers.ReadOnlyField(label=_("Is success")) + is_finished = serializers.ReadOnlyField(label=_("Is finished")) + time_cost = serializers.ReadOnlyField(label=_("Time cost")) class Meta: model = JobExecution - read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', - 'date_finished', - 'date_created', - 'is_success', 'task_id', 'short_id', 'job_type', 'summary', 'material'] + read_only_fields = ["id", "task_id", "timedelta", "time_cost", + 'is_finished', 'date_start', 'date_finished', + 'date_created', 'is_success', 'task_id', 'job_type', + 'summary', 'material'] fields = read_only_fields + [ "job", "parameters", "creator" ] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index bcfd75acd..a69bab3c9 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -2,7 +2,7 @@ import os from rest_framework import serializers -from common.drf.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField from ops.models import Playbook from orgs.mixins.serializers import BulkOrgResourceModelSerializer diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py index 29add5bcb..e72b88f25 100644 --- a/apps/ops/signal_handlers.py +++ b/apps/ops/signal_handlers.py @@ -105,4 +105,4 @@ def task_sent_handler(headers=None, body=None, **kwargs): 'kwargs': kwargs } CeleryTaskExecution.objects.create(**data) - CeleryTask.objects.filter(name=task).update(last_published_time=timezone.now()) + CeleryTask.objects.filter(name=task).update(date_last_publish=timezone.now()) diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 19eb488e7..4ba139577 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -2,10 +2,9 @@ import os import subprocess -from django.conf import settings from celery import shared_task - from celery.exceptions import SoftTimeLimitExceeded +from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -32,6 +31,15 @@ def run_ops_job(job_id): run_ops_job_execution(execution) +# +# @shared_task(soft_time_limit=60, queue="ansible") +# def show_env(): +# import json +# print(os.environ) +# data = json.dumps(dict(os.environ), indent=4) +# return data + + @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution")) def run_ops_job_execution(execution_id, **kwargs): execution = get_object_or_none(JobExecution, id=execution_id) diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 5d859b801..b0a4ccb14 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -25,7 +25,7 @@ router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-execut urlpatterns = [ path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'), path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'), - path('job-execution/task-detail/', api.JobExecutionTaskDetail.as_view(), name='task-detail'), + path('job-execution/task-detail//', api.JobExecutionTaskDetail.as_view(), name='task-detail'), path('frequent-username/', api.FrequentUsernames.as_view(), name='frequent-usernames'), path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'), diff --git a/apps/ops/utils.py b/apps/ops/utils.py index e8e6cadca..c2fb7e643 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -13,61 +13,6 @@ from .const import DEFAULT_PASSWORD_RULES logger = get_logger(__file__) -DEFAULT_TASK_OPTIONS = { - 'timeout': 10, - 'forks': 10, -} - - -def get_task_by_id(task_id): - return get_object_or_none(Task, id=task_id) - - -@org_aware_func("hosts") -def update_or_create_ansible_task( - task_name, hosts, tasks, - interval=None, crontab=None, is_periodic=False, - callback=None, pattern='all', options=None, - run_as_admin=False, run_as=None, system_user=None, become_info=None, -): - if not hosts or not tasks or not task_name: - return None, None - if options is None: - options = DEFAULT_TASK_OPTIONS - defaults = { - 'name': task_name, - 'interval': interval, - 'crontab': crontab, - 'is_periodic': is_periodic, - 'callback': callback, - } - - created = False - task, ok = Task.objects.update_or_create( - defaults=defaults, name=task_name - ) - adhoc = task.get_latest_adhoc() - new_adhoc = AdHoc(task=task, pattern=pattern, - run_as_admin=run_as_admin, - run_as=run_as, run_system_user=system_user) - new_adhoc.tasks = tasks - new_adhoc.options = options - new_adhoc.become = become_info - - hosts_same = True - if adhoc: - old_hosts = set([str(asset.id) for asset in adhoc.hosts.all()]) - new_hosts = set([str(asset.id) for asset in hosts]) - hosts_same = old_hosts == new_hosts - - if not adhoc or not adhoc.same_with(new_adhoc) or not hosts_same: - logger.debug(_("Update task content: {}").format(task_name)) - new_adhoc.save() - new_adhoc.hosts.set(hosts) - task.latest_adhoc = new_adhoc - created = True - return task, created - def get_task_log_path(base_path, task_id, level=2): task_id = str(task_id) @@ -81,14 +26,3 @@ def get_task_log_path(base_path, task_id, level=2): make_dirs(os.path.dirname(path), exist_ok=True) return path - -def generate_random_password(**kwargs): - import random - import string - length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) - symbol_set = kwargs.get('symbol_set') - if symbol_set is None: - symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] - chars = string.ascii_letters + string.digits + symbol_set - password = ''.join([random.choice(chars) for _ in range(length)]) - return password diff --git a/apps/ops/views.py b/apps/ops/views.py index fee4a0b9f..bb07d8cac 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -3,7 +3,7 @@ from django.views.generic import TemplateView from django.conf import settings -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin from rbac.permissions import RBACPermission __all__ = ['CeleryTaskLogView'] diff --git a/apps/ops/ws.py b/apps/ops/ws.py index 5c261f76f..c2386c77a 100644 --- a/apps/ops/ws.py +++ b/apps/ops/ws.py @@ -8,6 +8,7 @@ from common.db.utils import close_old_connections from common.utils import get_logger from .ansible.utils import get_ansible_task_log_path from .celery.utils import get_celery_task_log_path +from .const import CELERY_LOG_MAGIC_MARK logger = get_logger(__name__) @@ -52,7 +53,6 @@ class TaskLogWebsocket(AsyncJsonWebsocketConsumer): await self.send_json({'message': '\r\n'}) try: logger.debug('Task log path: {}'.format(log_path)) - task_end_mark = [] async with aiofiles.open(log_path, 'rb') as task_log_f: while not self.disconnected: data = await task_log_f.read(4096) @@ -61,18 +61,15 @@ class TaskLogWebsocket(AsyncJsonWebsocketConsumer): await self.send_json( {'message': data.decode(errors='ignore'), 'task': task_id} ) - if data.find(b'succeeded in') != -1: - task_end_mark.append(1) - if data.find(bytes(task_id, 'utf8')) != -1: - task_end_mark.append(1) - elif len(task_end_mark) == 2: - logger.debug('Task log end: {}'.format(task_id)) - await self.send_json({'event': 'end', 'task': task_id}) - break + if data.find(CELERY_LOG_MAGIC_MARK) != -1: + await self.send_json( + {'event': 'end', 'task': task_id, 'message': ''} + ) + logger.debug("Task log file magic mark found") + break await asyncio.sleep(0.2) except OSError as e: logger.warn('Task log path open failed: {}'.format(e)) - # await self.close() async def disconnect(self, close_code): self.disconnected = True diff --git a/apps/orgs/api.py b/apps/orgs/api.py index e18ddf55e..0452f2bff 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -7,28 +7,25 @@ from rest_framework_bulk import BulkModelViewSet from rest_framework.generics import RetrieveAPIView from rest_framework.exceptions import PermissionDenied +from common.utils import get_logger from common.permissions import IsValidUser +from users.models import User, UserGroup +from assets.models import ( + Asset, Domain, Label, Node, +) +from perms.models import AssetPermission +from orgs.utils import current_org, tmp_to_root_org from .models import Organization from .serializers import ( OrgSerializer, CurrentOrgSerializer ) -from users.models import User, UserGroup -from assets.models import ( - Asset, Domain, Label, Node, - CommandFilter, CommandFilterRule -) -from perms.models import AssetPermission -from orgs.utils import current_org, tmp_to_root_org -from common.utils import get_logger - logger = get_logger(__file__) - # 部分 org 相关的 model,需要清空这些数据之后才能删除该组织 org_related_models = [ User, UserGroup, Asset, Label, Domain, Node, Label, - CommandFilter, CommandFilterRule, AssetPermission, + AssetPermission, ] @@ -38,7 +35,7 @@ class OrgViewSet(BulkModelViewSet): queryset = Organization.objects.all() serializer_class = OrgSerializer ordering_fields = ('name',) - ordering = ('name', ) + ordering = ('name',) def get_serializer_class(self): mapper = { @@ -68,7 +65,8 @@ class OrgViewSet(BulkModelViewSet): if str(instance.id) == settings.AUTH_LDAP_SYNC_ORG_ID: msg = _( - 'LDAP synchronization is set to the current organization. Please switch to another organization before deleting' + 'LDAP synchronization is set to the current organization. ' + 'Please switch to another organization before deleting' ) raise PermissionDenied(detail=msg) diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index cd67984a3..1eb54e037 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -7,7 +7,8 @@ from common.cache import Cache, IntegerField from common.utils import get_logger from common.utils.timezone import local_zero_hour, local_monday from users.models import UserGroup, User -from assets.models import Node, Domain, Asset, Account +from assets.models import Node, Domain, Asset +from accounts.models import Account from terminal.models import Session from perms.models import AssetPermission diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index 7d9839a20..a0a5daddd 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # +from django.db.models import QuerySet from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework_bulk import BulkModelViewSet -from common.mixins import CommonApiMixin, RelationMixin +from common.api import CommonApiMixin, RelationMixin from orgs.utils import current_org - from ..utils import set_to_root_org __all__ = [ @@ -21,6 +21,10 @@ class RootOrgViewMixin: class OrgQuerySetMixin: + queryset: QuerySet + get_serializer_class: callable + action: str + def get_queryset(self): if hasattr(self, 'model'): queryset = self.model.objects.all() @@ -32,11 +36,7 @@ class OrgQuerySetMixin: queryset = super().get_queryset() if hasattr(self, 'swagger_fake_view'): - return queryset[:1] - if hasattr(self, 'action') and self.action == 'list': - serializer_class = self.get_serializer_class() - if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): - queryset = serializer_class.setup_eager_loading(queryset) + return queryset.none() return queryset diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py index 8f70814bc..7ff66cf59 100644 --- a/apps/orgs/mixins/serializers.py +++ b/apps/orgs/mixins/serializers.py @@ -5,17 +5,17 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from common.validators import ProjectUniqueValidator -from common.drf.serializers import BulkSerializerMixin, CommonSerializerMixin +from common.serializers import BulkSerializerMixin, CommonSerializerMixin, CommonModelSerializer, \ + CommonBulkModelSerializer from ..utils import get_current_org_id_for_serializer - __all__ = [ - "OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin", - "BulkOrgResourceModelSerializer", "OrgResourceModelSerializerMixin", + "OrgResourceSerializerMixin", "BulkOrgResourceModelSerializer", + "OrgResourceModelSerializerMixin", ] -class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer): +class OrgResourceSerializerMixin(serializers.Serializer): """ 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id (同时为serializer.is_valid()对Model的unique_together校验做准备) @@ -43,13 +43,9 @@ class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer): return fields -class OrgResourceModelSerializerMixin(OrgResourceSerializerMixin, serializers.ModelSerializer): +class OrgResourceModelSerializerMixin(OrgResourceSerializerMixin, CommonModelSerializer): pass -class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerMixin): - pass - - -class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): +class BulkOrgResourceModelSerializer(OrgResourceSerializerMixin, CommonBulkModelSerializer): pass diff --git a/apps/orgs/models.py b/apps/orgs/models.py index be88ec242..ba4669cce 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -3,7 +3,9 @@ from django.utils.translation import ugettext_lazy as _ from common.db.models import JMSBaseModel from common.tree import TreeNode -from common.utils import lazyproperty, settings +from common.utils import lazyproperty, settings, get_logger + +logger = get_logger(__name__) class OrgRoleMixin: @@ -126,7 +128,6 @@ class Organization(OrgRoleMixin, JMSBaseModel): @classmethod def expire_orgs_mapping(cls): - print("Expire orgs mapping: ") cls.orgs_mapping = None def org_id(self): @@ -141,6 +142,15 @@ class Organization(OrgRoleMixin, JMSBaseModel): obj.save() return obj + @classmethod + def system(cls): + defaults = dict(id=cls.SYSTEM_ID, name=cls.SYSTEM_NAME) + obj, created = cls.objects.get_or_create(defaults=defaults, id=cls.SYSTEM_ID) + if not obj.builtin: + obj.builtin = True + obj.save() + return obj + @classmethod def root(cls): name = settings.GLOBAL_ORG_DISPLAY_NAME or cls.ROOT_NAME diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 505422377..0acebf6b9 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -2,7 +2,8 @@ from django.db.models.signals import post_save, pre_delete, pre_save, post_delet from django.dispatch import receiver from orgs.models import Organization -from assets.models import Node, Account +from assets.models import Node +from accounts.models import Account from perms.models import AssetPermission from audits.models import UserLoginLog from users.models import UserGroup, User diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 38cd0e764..6813e9f0b 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -48,6 +48,10 @@ def set_to_root_org(): set_current_org(Organization.root()) +def set_to_system_org(): + set_current_org(Organization.system()) + + def _find(attr): return getattr(thread_local, attr, None) @@ -114,6 +118,7 @@ def filter_org_queryset(queryset): else: kwargs = {'org_id': org.id} + # import traceback # lines = traceback.format_stack() # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") # for line in lines[-10:-1]: diff --git a/apps/perms/api/asset_permission_relation.py b/apps/perms/api/asset_permission_relation.py index 5b85cb971..f2bd3cf30 100644 --- a/apps/perms/api/asset_permission_relation.py +++ b/apps/perms/api/asset_permission_relation.py @@ -10,7 +10,7 @@ from orgs.utils import current_org from perms import serializers from perms import models from perms.utils import AssetPermissionPermAssetUtil -from assets.serializers import AccountSerializer +from accounts.serializers import AccountSerializer __all__ = [ 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', @@ -123,5 +123,3 @@ class AssetPermissionAccountListApi(generics.ListAPIView): perm = get_object_or_404(models.AssetPermission, pk=pk) accounts = perm.get_all_accounts() return accounts - - diff --git a/apps/perms/api/user_permission/tree/asset.py b/apps/perms/api/user_permission/tree/asset.py index 081dbc756..c620356ad 100644 --- a/apps/perms/api/user_permission/tree/asset.py +++ b/apps/perms/api/user_permission/tree/asset.py @@ -5,14 +5,14 @@ from assets.models import Asset from assets.api import SerializeToTreeNodeMixin from common.utils import get_logger -from ..assets import UserDirectPermedAssetsApi +from ..assets import UserAllPermedAssetsApi from .mixin import RebuildTreeMixin logger = get_logger(__name__) __all__ = [ - 'UserDirectPermedAssetsAsTreeApi', + 'UserAllPermedAssetsAsTreeApi', 'UserUngroupAssetsAsTreeApi', ] @@ -35,12 +35,12 @@ class AssetTreeMixin(RebuildTreeMixin, SerializeToTreeNodeMixin): return Response(data=data) -class UserDirectPermedAssetsAsTreeApi(AssetTreeMixin, UserDirectPermedAssetsApi): +class UserAllPermedAssetsAsTreeApi(AssetTreeMixin, UserAllPermedAssetsApi): """ 用户 '直接授权的资产' 作为树 """ pass -class UserUngroupAssetsAsTreeApi(UserDirectPermedAssetsAsTreeApi): +class UserUngroupAssetsAsTreeApi(UserAllPermedAssetsAsTreeApi): """ 用户 '未分组节点的资产(直接授权的资产)' 作为树 """ def get_assets(self): if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: diff --git a/apps/perms/api/user_permission/tree/mixin.py b/apps/perms/api/user_permission/tree/mixin.py index c0cafe37c..46714ce05 100644 --- a/apps/perms/api/user_permission/tree/mixin.py +++ b/apps/perms/api/user_permission/tree/mixin.py @@ -2,7 +2,7 @@ from django.core.cache import cache from rest_framework.request import Request -from common.http import is_true +from common.utils.http import is_true from common.utils import lazyproperty from perms.utils import UserPermTreeRefreshUtil from users.models import User @@ -36,5 +36,5 @@ class RebuildTreeMixin: cache.delete(force_cache_key) else: force = False - cache.set(force_cache_key, count+1, force_timeout) + cache.set(force_cache_key, count + 1, force_timeout) return force diff --git a/apps/perms/api/user_permission/tree/node_with_asset.py b/apps/perms/api/user_permission/tree/node_with_asset.py index 9aa73963b..576a5615d 100644 --- a/apps/perms/api/user_permission/tree/node_with_asset.py +++ b/apps/perms/api/user_permission/tree/node_with_asset.py @@ -10,9 +10,10 @@ from rest_framework.generics import get_object_or_404 from rest_framework.exceptions import PermissionDenied, NotFound from assets.utils import KubernetesTree -from assets.models import Asset, Account -from assets.const import AliasAccount +from assets.models import Asset +from accounts.const import AliasAccount from assets.api import SerializeToTreeNodeMixin +from accounts.models import Account from authentication.models import ConnectionToken from common.utils import get_object_or_none, lazyproperty from common.utils.common import timeit @@ -151,13 +152,13 @@ class UserGrantedK8sAsTreeApi(SelfOrPKUserMixin, ListAPIView): def get_account_secret(self, token: ConnectionToken): util = PermAccountUtil() accounts = util.get_permed_accounts_for_user(self.user, token.asset) - account_username = token.account - accounts = filter(lambda x: x.username == account_username, accounts) + account_name = token.account + accounts = filter(lambda x: x.name == account_name, accounts) accounts = list(accounts) if not accounts: raise NotFound('Account is not found') account = accounts[0] - if account.username in [ + if account.name in [ AliasAccount.INPUT, AliasAccount.USER ]: return token.input_secret diff --git a/apps/perms/filters.py b/apps/perms/filters.py index 8743c38be..56382cf2d 100644 --- a/apps/perms/filters.py +++ b/apps/perms/filters.py @@ -2,7 +2,7 @@ from django_filters import rest_framework as filters from django.db.models import QuerySet, Q from common.drf.filters import BaseFilterSet -from common.utils import get_object_or_none +from common.utils import get_object_or_none, is_uuid from users.models import User, UserGroup from assets.models import Node, Asset from perms.models import AssetPermission @@ -91,7 +91,7 @@ class PermissionBaseFilter(BaseFilterSet): class AssetPermissionFilter(PermissionBaseFilter): is_effective = filters.BooleanFilter(method='do_nothing') node_id = filters.UUIDFilter(method='do_nothing') - node = filters.CharFilter(method='do_nothing') + node_name = filters.CharFilter(method='do_nothing') asset_id = filters.UUIDFilter(method='do_nothing') asset_name = filters.CharFilter(method='do_nothing') ip = filters.CharFilter(method='do_nothing') @@ -100,8 +100,9 @@ class AssetPermissionFilter(PermissionBaseFilter): model = AssetPermission fields = ( 'user_id', 'username', 'user_group_id', - 'user_group', 'node_id', 'node', 'asset_id', 'name', 'ip', 'name', - 'all', 'asset_id', 'is_valid', 'is_effective', 'from_ticket' + 'user_group', 'node_id', 'node_name', 'asset_id', 'asset_name', + 'name', 'ip', 'name', + 'all', 'is_valid', 'is_effective', 'from_ticket' ) @property @@ -116,7 +117,7 @@ class AssetPermissionFilter(PermissionBaseFilter): def filter_node(self, queryset: QuerySet): is_query_all = self.get_query_param('all', True) node_id = self.get_query_param('node_id') - node_name = self.get_query_param('node') + node_name = self.get_query_param('node_name') if node_id: _nodes = Node.objects.filter(pk=node_id) elif node_name: diff --git a/apps/perms/hands.py b/apps/perms/hands.py index dabb9f7c0..dd6372ed9 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -2,12 +2,11 @@ # from users.models import User, UserGroup -from assets.models import Asset, Node, Label, FavoriteAsset, Account +from assets.models import Asset, Node, Label, FavoriteAsset from assets.serializers import NodeSerializer __all__ = [ 'User', 'UserGroup', 'Asset', 'Node', 'Label', 'FavoriteAsset', - 'NodeSerializer', 'Account' + 'NodeSerializer', ] - diff --git a/apps/perms/migrations/0032_auto_20221111_1919.py b/apps/perms/migrations/0032_auto_20221111_1919.py index 3f3c56533..6ab787df0 100644 --- a/apps/perms/migrations/0032_auto_20221111_1919.py +++ b/apps/perms/migrations/0032_auto_20221111_1919.py @@ -4,9 +4,9 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assets', '0111_alter_automationexecution_status'), + ('assets', '0101_auto_20220811_1511'), + ('accounts', '0001_initial'), ('perms', '0031_auto_20220816_1600'), ] @@ -21,11 +21,11 @@ class Migration(migrations.Migration): 'indexes': [], 'constraints': [], }, - bases=('assets.account',), + bases=('accounts.account',), ), migrations.AlterField( model_name='assetpermission', name='actions', - field=models.IntegerField(default=0, verbose_name='Actions'), + field=models.IntegerField(default=1, verbose_name='Actions'), ), ] diff --git a/apps/perms/migrations/0033_alter_assetpermission_actions.py b/apps/perms/migrations/0033_alter_assetpermission_actions.py deleted file mode 100644 index cfa39f6e3..000000000 --- a/apps/perms/migrations/0033_alter_assetpermission_actions.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-18 02:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0032_auto_20221111_1919'), - ] - - operations = [ - migrations.AlterField( - model_name='assetpermission', - name='actions', - field=models.IntegerField(default=1, verbose_name='Actions'), - ), - ] diff --git a/apps/perms/migrations/0034_auto_20221220_1956.py b/apps/perms/migrations/0033_auto_20221220_1956.py similarity index 96% rename from apps/perms/migrations/0034_auto_20221220_1956.py rename to apps/perms/migrations/0033_auto_20221220_1956.py index 41f6528d1..1c10257d2 100644 --- a/apps/perms/migrations/0034_auto_20221220_1956.py +++ b/apps/perms/migrations/0033_auto_20221220_1956.py @@ -5,9 +5,8 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('perms', '0033_alter_assetpermission_actions'), + ('perms', '0032_auto_20221111_1919'), ] operations = [ diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 2af68832c..1f14c86fa 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -6,14 +6,14 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from users.models import User -from assets.models import Asset, Account +from assets.models import Asset +from accounts.models import Account from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import OrgManager from common.utils import date_expired_default from common.utils.timezone import local_now - from perms.const import ActionChoices -from assets.const import AliasAccount +from accounts.const import AliasAccount __all__ = ['AssetPermission', 'ActionChoices'] diff --git a/apps/perms/models/perm_node.py b/apps/perms/models/perm_node.py index 1db0f2fe7..e51851db9 100644 --- a/apps/perms/models/perm_node.py +++ b/apps/perms/models/perm_node.py @@ -2,7 +2,8 @@ from django.db import models from django.db.models import F, TextChoices from django.utils.translation import ugettext_lazy as _ -from assets.models import Asset, Node, FamilyMixin, Account +from assets.models import Asset, Node, FamilyMixin +from accounts.models import Account from common.utils import lazyproperty from orgs.mixins.models import JMSOrgBaseModel diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 673b567c5..63ba3a765 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.models import Asset, Node -from common.drf.fields import BitChoicesField, ObjectRelatedField +from common.serializers.fields import BitChoicesField, ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import ActionChoices, AssetPermission from users.models import User, UserGroup @@ -34,26 +34,27 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): class Meta: model = AssetPermission fields_mini = ["id", "name"] - fields_small = fields_mini + [ + fields_generic = [ "accounts", - "is_active", - "is_expired", - "is_valid", "actions", "created_by", "date_created", - "date_expired", "date_start", + "date_expired", + "is_active", + "is_expired", + "is_valid", "comment", "from_ticket", ] + fields_small = fields_mini + fields_generic fields_m2m = [ "users", "user_groups", "assets", "nodes", ] - fields = fields_small + fields_m2m + fields = fields_mini + fields_m2m + fields_generic read_only_fields = ["created_by", "date_created", "from_ticket"] extra_kwargs = { "actions": {"label": _("Actions")}, diff --git a/apps/perms/serializers/permission_relation.py b/apps/perms/serializers/permission_relation.py index 3e469106a..c91726a14 100644 --- a/apps/perms/serializers/permission_relation.py +++ b/apps/perms/serializers/permission_relation.py @@ -2,7 +2,7 @@ # from rest_framework import serializers -from common.drf.serializers import BulkSerializerMixin +from common.serializers import BulkSerializerMixin from perms.models import AssetPermission __all__ = [ diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 9b4ad26f0..8e22623fc 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -5,10 +5,12 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.const import Category, AllTypes -from assets.models import Node, Asset, Platform, Account +from assets.models import Node, Asset, Platform +from accounts.models import Account from assets.serializers.asset.common import AssetProtocolsSerializer -from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from perms.serializers.permission import ActionChoicesField +from orgs.mixins.serializers import OrgResourceModelSerializerMixin __all__ = [ 'NodePermedSerializer', 'AssetPermedSerializer', @@ -16,12 +18,13 @@ __all__ = [ ] -class AssetPermedSerializer(serializers.ModelSerializer): +class AssetPermedSerializer(OrgResourceModelSerializerMixin): """ 被授权资产的数据结构 """ platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) + domain = ObjectRelatedField(required=False, queryset=Node.objects, label=_('Domain')) class Meta: model = Asset diff --git a/apps/perms/urls/user_permission.py b/apps/perms/urls/user_permission.py index 63ed4f636..ba172747c 100644 --- a/apps/perms/urls/user_permission.py +++ b/apps/perms/urls/user_permission.py @@ -21,7 +21,7 @@ user_permission_urlpatterns = [ name='user-node-children'), # tree-asset - path('/assets/tree/', api.UserDirectPermedAssetsAsTreeApi.as_view(), + path('/assets/tree/', api.UserAllPermedAssetsAsTreeApi.as_view(), name='user-direct-assets-as-tree'), path('/ungroup/assets/tree/', api.UserUngroupAssetsAsTreeApi.as_view(), name='user-ungroup-assets-as-tree'), diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index bdec2b934..5973ea4d7 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,7 +1,7 @@ from collections import defaultdict -from assets.models import Account -from assets.const import AliasAccount +from accounts.models import Account +from accounts.const import AliasAccount from .permission import AssetPermissionUtil __all__ = ['PermAccountUtil'] diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py index bbec44343..d3d1562c0 100644 --- a/apps/perms/utils/permission.py +++ b/apps/perms/utils/permission.py @@ -25,9 +25,9 @@ class AssetPermissionUtil(object): groups = user.groups.all() group_perm_ids = self.get_permissions_for_user_groups(groups, flat=True) perm_ids.update(group_perm_ids) - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_user_groups(self, user_groups, flat=False): @@ -39,9 +39,9 @@ class AssetPermissionUtil(object): perm_ids = AssetPermission.user_groups.through.objects \ .filter(usergroup_id__in=group_ids) \ .values_list('assetpermission_id', flat=True).distinct() - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_assets(self, assets, with_node=True, flat=False): @@ -56,9 +56,9 @@ class AssetPermissionUtil(object): nodes = Asset.get_all_nodes_for_assets(assets) node_perm_ids = self.get_permissions_for_nodes(nodes, flat=True) perm_ids.update(node_perm_ids) - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_nodes(self, nodes, with_ancestor=False, flat=False): @@ -69,9 +69,9 @@ class AssetPermissionUtil(object): node_ids = nodes.values_list('id', flat=True).distinct() relations = AssetPermission.nodes.through.objects.filter(node_id__in=node_ids) perm_ids = relations.values_list('assetpermission_id', flat=True).distinct() - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_user_asset(self, user, asset): @@ -103,5 +103,5 @@ class AssetPermissionUtil(object): @staticmethod def get_permissions(ids): - perms = AssetPermission.objects.filter(id__in=ids).order_by('-date_expired') + perms = AssetPermission.objects.filter(id__in=ids).valid().order_by('-date_expired') return perms diff --git a/apps/perms/utils/user_perm.py b/apps/perms/utils/user_perm.py index ceea59b22..d7a3ea06a 100644 --- a/apps/perms/utils/user_perm.py +++ b/apps/perms/utils/user_perm.py @@ -82,7 +82,7 @@ class UserPermAssetUtil(AssetPermissionPermAssetUtil): node = PermNode.objects.get(id=node_id) node.compute_node_from_and_assets_amount(self.user) if node.node_from == node.NodeFrom.granted: - assets = PermNode.get_nodes_all_assets() + assets = PermNode.get_nodes_all_assets(node) elif node.node_from in (node.NodeFrom.asset, node.NodeFrom.child): node.assets_amount = node.granted_assets_amount assets = self._get_indirect_perm_node_all_assets(node) diff --git a/apps/rbac/api/permission.py b/apps/rbac/api/permission.py index 722deb556..9500af3ac 100644 --- a/apps/rbac/api/permission.py +++ b/apps/rbac/api/permission.py @@ -3,7 +3,7 @@ from rest_framework.decorators import action from django.shortcuts import get_object_or_404 from common.tree import TreeNodeSerializer -from common.drf.api import JMSModelViewSet +from common.api import JMSModelViewSet from ..models import Permission, Role from ..serializers import PermissionSerializer diff --git a/apps/rbac/api/role.py b/apps/rbac/api/role.py index be8274e91..0a4d52706 100644 --- a/apps/rbac/api/role.py +++ b/apps/rbac/api/role.py @@ -3,8 +3,8 @@ from django.utils.translation import ugettext as _ from rest_framework.exceptions import PermissionDenied from rest_framework.decorators import action -from common.drf.api import JMSModelViewSet -from common.mixins.api import PaginatedResponseMixin +from common.api import JMSModelViewSet +from common.api import PaginatedResponseMixin from ..filters import RoleFilter from ..serializers import RoleSerializer, RoleUserSerializer from ..models import Role, SystemRole, OrgRole @@ -117,4 +117,3 @@ class OrgRolePermissionsViewSet(BaseRolePermissionsViewSet): rbac_perms = ( ('get_tree', 'rbac.view_permission'), ) - diff --git a/apps/rbac/backends.py b/apps/rbac/backends.py index 76ebd1d70..aac44da36 100644 --- a/apps/rbac/backends.py +++ b/apps/rbac/backends.py @@ -20,7 +20,12 @@ class RBACBackend(JMSBaseAuthBackend): raise PermissionDenied() if perm == '*': return True - perm_set = set(i.strip() for i in perm.split('|')) + if isinstance(perm, str): + perm_set = set(i.strip() for i in perm.split('|')) + elif isinstance(perm, (list, tuple, set)): + perm_set = set(perm) + else: + raise ValueError('perm must be str, list, tuple or set') has_perm = bool(perm_set & set(user_obj.perms)) if not has_perm: raise PermissionDenied() diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py index 08e54374c..c3f748a27 100644 --- a/apps/rbac/builtin.py +++ b/apps/rbac/builtin.py @@ -19,7 +19,6 @@ user_perms = ( ('assets', 'systemuser', 'match', 'systemuser'), ('assets', 'node', 'match', 'node'), ('applications', 'application', 'match', 'application'), - ('ops', 'commandexecution', 'add', 'commandexecution'), ) system_user_perms = ( @@ -36,7 +35,6 @@ _auditor_perms = ( ('terminal', 'sessionreplay', 'view,download', 'sessionreplay'), ('terminal', 'session', '*', '*'), ('terminal', 'command', '*', '*'), - ('ops', 'commandexecution', 'view', 'commandexecution'), ) auditor_perms = user_perms + _auditor_perms diff --git a/apps/rbac/const.py b/apps/rbac/const.py index b81ec1bf9..bbdfe1f28 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -30,33 +30,32 @@ exclude_permissions = ( ('assets', 'adminuser', '*', '*'), ('assets', 'assetgroup', '*', '*'), ('assets', 'cluster', '*', '*'), + ('assets', 'systemuser', '*', '*'), ('assets', 'favoriteasset', '*', '*'), - ('assets', 'historicalaccount', '*', '*'), ('assets', 'assetuser', '*', '*'), - ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), - ('assets', 'accountbackupplanexecution', 'delete,change', 'accountbackupplanexecution'), - ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), ('assets', 'web', '*', '*'), ('assets', 'host', '*', '*'), ('assets', 'cloud', '*', '*'), ('assets', 'device', '*', '*'), ('assets', 'database', '*', '*'), ('assets', 'protocol', '*', '*'), - ('assets', 'systemuser', '*', '*'), ('assets', 'baseautomation', '*', '*'), + ('assets', 'assetbaseautomation', '*', '*'), + ('assets', 'automationexecution', '*', '*'), ('assets', 'pingautomation', '*', '*'), ('assets', 'platformprotocol', '*', '*'), ('assets', 'platformautomation', '*', '*'), - ('assets', 'gatherfactsautomation', '*', '*'), - ('assets', 'pushaccountautomation', '*', '*'), ('assets', 'verifyaccountautomation', '*', '*'), - ('assets', 'changesecretrecord', 'add,delete,change', 'changesecretrecord'), - ('assets', 'automationexecution', '*', 'automationexecution'), + ('assets', 'gatherfactsautomation', '*', '*'), ('assets', 'commandfilter', '*', '*'), ('assets', 'commandfilterrule', '*', '*'), - # TODO 暂时去掉历史账号的权限 - ('assets', 'account', '*', 'assethistoryaccount'), - ('assets', 'account', '*', 'assethistoryaccountsecret'), + + ('accounts', 'historicalaccount', '*', '*'), + ('accounts', 'accountbaseautomation', '*', '*'), + ('accounts', 'verifyaccountautomation', '*', '*'), + ('accounts', 'automationexecution', '*', 'automationexecution'), + ('accounts', 'accountbackupexecution', 'delete,change', 'accountbackupexecution'), + ('accounts', 'changesecretrecord', 'add,delete,change', 'changesecretrecord'), ('perms', 'userassetgrantedtreenoderelation', '*', '*'), ('perms', 'usergrantedmappingnode', '*', '*'), @@ -69,6 +68,7 @@ exclude_permissions = ( ('rbac', 'rolebinding', '*', '*'), ('rbac', 'systemrolebinding', 'change', 'systemrolebinding'), ('rbac', 'orgrolebinding', 'change', 'orgrolebinding'), + ('rbac', 'menupermission', '*', 'menupermission'), ('rbac', 'role', '*', '*'), ('ops', 'adhoc', 'delete,change', '*'), ('ops', 'adhocexecution', 'add,delete,change', '*'), @@ -76,7 +76,6 @@ exclude_permissions = ( ('ops', 'historicaljob', '*', '*'), ('ops', 'celerytask', 'add,change,delete', 'celerytask'), ('ops', 'celerytaskexecution', 'add,change,delete', 'celerytaskexecution'), - ('ops', 'commandexecution', 'delete,change', 'commandexecution'), ('orgs', 'organizationmember', '*', '*'), ('settings', 'setting', 'add,change,delete', 'setting'), ('audits', 'operatelog', 'add,delete,change', 'operatelog'), @@ -114,7 +113,6 @@ exclude_permissions = ( ('applications', '*', '*', '*'), ) - only_system_permissions = ( ('assets', 'platform', 'add,change,delete', 'platform'), ('users', 'user', 'delete', 'user'), diff --git a/apps/rbac/migrations/0011_remove_redundant_permission.py b/apps/rbac/migrations/0011_remove_redundant_permission.py index 74d8412d4..26b5df847 100644 --- a/apps/rbac/migrations/0011_remove_redundant_permission.py +++ b/apps/rbac/migrations/0011_remove_redundant_permission.py @@ -6,7 +6,9 @@ from django.db import migrations def migrate_remove_redundant_permission(apps, *args): model = apps.get_model('rbac', 'ContentType') model.objects.filter(app_label='applications').delete() - model.objects.filter(app_label='ops', model='task').delete() + model.objects.filter(app_label='ops', model__in=[ + 'task', 'commandexecution' + ]).delete() model.objects.filter(app_label='xpack', model__in=[ 'applicationchangeauthplan', 'applicationchangeauthplanexecution', @@ -15,13 +17,19 @@ def migrate_remove_redundant_permission(apps, *args): ]).delete() model.objects.filter(app_label='assets', model__in=[ - 'authbook', 'historicalauthbook' + 'authbook', 'historicalauthbook', 'test_gateway', + 'accountbackupplan', 'accountbackupplanexecution', 'gathereduser', 'systemuser' ]).delete() model.objects.filter(app_label='perms', model__in=[ 'applicationpermission', 'permedapplication', 'commandfilterrule', 'historicalauthbook' ]).delete() + perm_model = apps.get_model('auth', 'Permission') + perm_model.objects.filter(codename__in=[ + 'view_permusergroupasset', 'view_permuserasset', 'push_assetsystemuser' + ]).delete() + class Migration(migrations.Migration): dependencies = [ diff --git a/apps/rbac/serializers/role.py b/apps/rbac/serializers/role.py index 140a01401..78724c18a 100644 --- a/apps/rbac/serializers/role.py +++ b/apps/rbac/serializers/role.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from users.models import User from ..models import Role diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index 14ed53343..f8309da36 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -48,6 +48,7 @@ extra_nodes_data = [ {"id": "cloud_import", "name": _("Cloud import"), "pId": "assets"}, {"id": "backup_account_node", "name": _("Backup account"), "pId": "accounts"}, {"id": "gather_account_node", "name": _("Gather account"), "pId": "accounts"}, + {"id": "push_account_node", "name": _("Push account"), "pId": "accounts"}, {"id": "asset_change_plan_node", "name": _("Asset change auth"), "pId": "accounts"}, {"id": "terminal_node", "name": _("Terminal setting"), "pId": "view_setting"}, {'id': "task_center", "name": _("Task Center"), "pId": "view_console"}, @@ -71,19 +72,18 @@ special_pid_mapper = { 'xpack.syncinstancetaskexecution': 'cloud_import', 'terminal.applet': 'remote_application', 'terminal.applethost': 'remote_application', - 'assets.accountbackupplan': "backup_account_node", - 'assets.accountbackupplanexecution': "backup_account_node", - 'xpack.changeauthplan': 'asset_change_plan_node', - 'xpack.changeauthplanexecution': 'asset_change_plan_node', - 'xpack.changeauthplantask': 'asset_change_plan_node', - "assets.gathereduser": "gather_account_node", - "assets.gatheraccountsautomation": "gather_account_node", - "assets.view_gatheraccountsexecution": "gather_account_node", - "assets.add_gatheraccountsexecution": "gather_account_node", - "assets.changesecretautomation": "asset_change_plan_node", - "assets.view_changesecretexecution": "asset_change_plan_node", - "assets.add_changesecretexection": "asset_change_plan_node", - "assets.view_changesecretrecord": "asset_change_plan_node", + 'accounts.accountbackupautomation': "backup_account_node", + 'accounts.accountbackupexecution': "backup_account_node", + "accounts.pushaccountautomation": "push_account_node", + "accounts.view_pushaccountexecution": "push_account_node", + "accounts.add_pushaccountexecution": "push_account_node", + "accounts.gatheraccountsautomation": "gather_account_node", + "accounts.view_gatheraccountsexecution": "gather_account_node", + "accounts.add_gatheraccountsexecution": "gather_account_node", + "accounts.changesecretautomation": "asset_change_plan_node", + "accounts.view_changesecretexecution": "asset_change_plan_node", + "accounts.add_changesecretexection": "asset_change_plan_node", + "accounts.view_changesecretrecord": "asset_change_plan_node", 'orgs.organization': 'view_setting', 'settings.setting': 'view_setting', 'terminal.terminal': 'terminal_node', @@ -95,8 +95,6 @@ special_pid_mapper = { 'terminal.endpointrule': 'terminal_node', 'audits.ftplog': 'terminal', 'perms.view_myassets': 'my_assets', - 'ops.add_commandexecution': 'view_workbench', - 'ops.view_commandexecution': 'audits', 'ops.jobauditlog': 'audits', 'ops.view_celerytask': 'task_center', 'ops.view_celerytaskexecution': 'task_center', @@ -123,7 +121,6 @@ verbose_name_mapper = { 'tickets.view_ticket': _("Ticket"), 'settings.setting': _("Common setting"), 'rbac.view_permission': _('View permission tree'), - 'ops.add_commandexecution': _('Execute batch command') } xpack_nodes = [ diff --git a/apps/settings/serializers/auth/dingtalk.py b/apps/settings/serializers/auth/dingtalk.py index ac22cc056..8df199eea 100644 --- a/apps/settings/serializers/auth/dingtalk.py +++ b/apps/settings/serializers/auth/dingtalk.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['DingTalkSettingSerializer'] diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index 0f5ba93d6..580cdb12a 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['FeiShuSettingSerializer'] @@ -12,4 +12,3 @@ class FeiShuSettingSerializer(serializers.Serializer): FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) - diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py index 2e371c388..a6cc22455 100644 --- a/apps/settings/serializers/auth/ldap.py +++ b/apps/settings/serializers/auth/ldap.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = [ 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer', diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py index 42c310333..a689243ef 100644 --- a/apps/settings/serializers/auth/oauth2.py +++ b/apps/settings/serializers/auth/oauth2.py @@ -1,8 +1,7 @@ - from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from common.utils import static_or_direct __all__ = [ diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index 259cf9712..f5c71d7a8 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = [ 'OIDCSettingSerializer', 'KeycloakSettingSerializer', @@ -97,4 +97,3 @@ class OIDCSettingSerializer(KeycloakSettingSerializer): AUTH_OPENID_ALWAYS_UPDATE_USER = serializers.BooleanField( required=False, label=_('Always update user') ) - diff --git a/apps/settings/serializers/auth/radius.py b/apps/settings/serializers/auth/radius.py index 6ef574eef..c9eb2e83e 100644 --- a/apps/settings/serializers/auth/radius.py +++ b/apps/settings/serializers/auth/radius.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['RadiusSettingSerializer'] diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py index 1696be723..cb5085386 100644 --- a/apps/settings/serializers/auth/sms.py +++ b/apps/settings/serializers/auth/sms.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from common.validators import PhoneValidator from common.sdk.sms import BACKENDS diff --git a/apps/settings/serializers/auth/wecom.py b/apps/settings/serializers/auth/wecom.py index 38516e790..a0b216a9d 100644 --- a/apps/settings/serializers/auth/wecom.py +++ b/apps/settings/serializers/auth/wecom.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['WeComSettingSerializer'] diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py index 6604eba6a..ab8b10415 100644 --- a/apps/settings/serializers/email.py +++ b/apps/settings/serializers/email.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer'] @@ -62,7 +62,8 @@ class EmailContentSettingSerializer(serializers.Serializer): EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField( max_length=4096, allow_blank=True, required=False, label=_('Create user email content'), - help_text=_('Tips: When creating a user, send the content of the email, support {username} {name} {email} label') + help_text=_( + 'Tips: When creating a user, send the content of the email, support {username} {name} {email} label') ) EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField( max_length=512, allow_blank=True, required=False, label=_('Signature'), diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 25f1d887f..f18b6f2a5 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -111,6 +111,11 @@ class SecurityAuthSerializer(serializers.Serializer): "Unit: second, The verification MFA takes effect only when you view the account password" ) ) + VERIFY_CODE_TTL = serializers.IntegerField( + min_value=5, max_value=60 * 60 * 10, + label=_("Verify code TTL"), + help_text=_("Unit: second") + ) SECURITY_LOGIN_CHALLENGE_ENABLED = serializers.BooleanField( required=False, default=False, label=_("Enable Login dynamic code"), @@ -174,7 +179,7 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri help_text=_('Multiple user using , split') ) SECURITY_COMMAND_EXECUTION = serializers.BooleanField( - required=False, label=_('Batch command execution'), + required=False, label=_('Operation center'), help_text=_('Allow user run batch command or not using ansible') ) SECURITY_SESSION_SHARE = serializers.BooleanField( diff --git a/apps/static/img/login_image.png b/apps/static/img/login_image.png index 936bcd81b..1cb86eb5f 100644 Binary files a/apps/static/img/login_image.png and b/apps/static/img/login_image.png differ diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index fddf90ad1..3b85ab753 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -6,13 +6,15 @@ from typing import Callable from django.conf import settings from django.core.files.storage import default_storage from django.http import HttpResponse +from django.shortcuts import get_object_or_404 from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ValidationError -from common.drf.serializers import FileSerializer +from common.api import JMSBulkModelViewSet +from common.serializers import FileSerializer from common.utils import is_uuid from terminal import serializers from terminal.models import AppletPublication, Applet @@ -82,7 +84,7 @@ class DownloadUploadMixin: return response -class AppletViewSet(DownloadUploadMixin, viewsets.ModelViewSet): +class AppletViewSet(DownloadUploadMixin, JMSBulkModelViewSet): queryset = Applet.objects.all() serializer_class = serializers.AppletSerializer rbac_perms = { @@ -93,9 +95,9 @@ class AppletViewSet(DownloadUploadMixin, viewsets.ModelViewSet): def get_object(self): pk = self.kwargs.get('pk') if not is_uuid(pk): - return self.queryset.get(name=pk) + return get_object_or_404(Applet, name=pk) else: - return self.queryset.get(pk=pk) + return get_object_or_404(Applet, pk=pk) def perform_destroy(self, instance): if not instance.name: diff --git a/apps/terminal/api/applet/host.py b/apps/terminal/api/applet/host.py index 4542f3b8d..40c0c6caf 100644 --- a/apps/terminal/api/applet/host.py +++ b/apps/terminal/api/applet/host.py @@ -2,7 +2,7 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response -from common.drf.api import JMSModelViewSet +from common.api import JMSBulkModelViewSet from common.permissions import IsServiceAccount from orgs.utils import tmp_to_builtin_org from terminal.models import AppletHost, AppletHostDeployment @@ -15,9 +15,10 @@ from terminal.tasks import run_applet_host_deployment, run_applet_host_deploymen __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet'] -class AppletHostViewSet(JMSModelViewSet): +class AppletHostViewSet(JMSBulkModelViewSet): serializer_class = AppletHostSerializer queryset = AppletHost.objects.all() + search_fields = ['asset_ptr__name', 'asset_ptr__address', ] def dispatch(self, request, *args, **kwargs): with tmp_to_builtin_org(system=1): @@ -40,6 +41,7 @@ class AppletHostViewSet(JMSModelViewSet): class AppletHostDeploymentViewSet(viewsets.ModelViewSet): serializer_class = AppletHostDeploymentSerializer queryset = AppletHostDeployment.objects.all() + filterset_fields = ['host', ] rbac_perms = ( ('applets', 'terminal.view_AppletHostDeployment'), ) diff --git a/apps/terminal/api/applet/relation.py b/apps/terminal/api/applet/relation.py index e31c6ba5c..8028ff1c8 100644 --- a/apps/terminal/api/applet/relation.py +++ b/apps/terminal/api/applet/relation.py @@ -6,7 +6,7 @@ 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.api import JMSModelViewSet from common.permissions import IsServiceAccount from common.utils import is_uuid from orgs.utils import tmp_to_builtin_org diff --git a/apps/terminal/api/component/endpoint.py b/apps/terminal/api/component/endpoint.py index 43ef3bba6..3b34f4a83 100644 --- a/apps/terminal/api/component/endpoint.py +++ b/apps/terminal/api/component/endpoint.py @@ -6,7 +6,7 @@ from rest_framework.request import Request from rest_framework.response import Response from assets.models import Asset -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet from common.permissions import IsValidUserOrConnectionToken from orgs.utils import tmp_to_root_org from terminal import serializers diff --git a/apps/terminal/api/component/terminal.py b/apps/terminal/api/component/terminal.py index d32adf02b..e2e1ad22f 100644 --- a/apps/terminal/api/component/terminal.py +++ b/apps/terminal/api/component/terminal.py @@ -8,7 +8,7 @@ from rest_framework import generics from rest_framework import status from rest_framework.views import APIView, Response -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet from common.exceptions import JMSException from common.permissions import WithBootstrapToken from terminal import serializers diff --git a/apps/terminal/api/db_listen_port.py b/apps/terminal/api/db_listen_port.py index 69dee6229..d06083768 100644 --- a/apps/terminal/api/db_listen_port.py +++ b/apps/terminal/api/db_listen_port.py @@ -5,25 +5,20 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from assets.serializers.asset.database import DatabaseWithGatewaySerializer +from orgs.utils import tmp_to_org from ..utils import db_port_manager, DBPortManager -from applications import serializers -from assets.serializers.asset.database import DatabaseSerializer - db_port_manager: DBPortManager - __all__ = ['DBListenPortViewSet'] class DBListenPortViewSet(GenericViewSet): rbac_perms = { - 'GET': 'applications.view_application', - 'list': 'applications.view_application', - 'db_info': 'applications.view_application', + '*': ['assets.view_asset'], } - - http_method_names = ['get', 'post'] + http_method_names = ['get'] def list(self, request, *args, **kwargs): ports = db_port_manager.get_already_use_ports() @@ -33,5 +28,7 @@ class DBListenPortViewSet(GenericViewSet): def db_info(self, request, *args, **kwargs): port = request.query_params.get("port") db = db_port_manager.get_db_by_port(port) - serializer = DatabaseSerializer(instance=db) - return Response(data=serializer.data, status=status.HTTP_200_OK) + + with tmp_to_org(db.org): + serializer = DatabaseWithGatewaySerializer(instance=db) + return Response(data=serializer.data, status=status.HTTP_200_OK) diff --git a/apps/terminal/api/session/command.py b/apps/terminal/api/session/command.py index 53fc0ca8f..3858aca07 100644 --- a/apps/terminal/api/session/command.py +++ b/apps/terminal/api/session/command.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from terminal.models import CommandStorage, Session, Command from terminal.filters import CommandFilter from orgs.utils import current_org -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet from common.utils import get_logger from terminal.backends.command.serializers import InsecureCommandAlertSerializer from terminal.exceptions import StorageInvalid @@ -104,7 +104,7 @@ class CommandViewSet(JMSBulkModelViewSet): filterset_class = CommandFilter search_fields = ('input',) model = Command - ordering_fields = ('timestamp', ) + ordering_fields = ('timestamp',) def merge_all_storage_list(self, request, *args, **kwargs): merged_commands = [] diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index 25475cdc4..6f4f2863f 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -18,7 +18,7 @@ from rest_framework.response import Response from common.const.http import GET from common.drf.filters import DatetimeRangeFilter from common.drf.renders import PassthroughRenderer -from common.mixins.api import AsyncApiMixin +from common.api import AsyncApiMixin from common.utils import data_to_json from common.utils import get_logger, get_object_or_none from orgs.mixins.api import OrgBulkModelViewSet diff --git a/apps/terminal/applets/chrome/app.py b/apps/terminal/applets/chrome/app.py index 78e3e2c5a..89ae3fc3b 100644 --- a/apps/terminal/applets/chrome/app.py +++ b/apps/terminal/applets/chrome/app.py @@ -3,13 +3,13 @@ from enum import Enum from subprocess import CREATE_NO_WINDOW from selenium import webdriver -from selenium.webdriver.remote.webelement import WebElement -from selenium.webdriver.common.by import By from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement -from common import (Asset, User, Account, Platform) -from common import (notify_err_message, block_input, unblock_input) +from common import (Asset, User, Account, Platform, Step) from common import (BaseApplication) +from common import (notify_err_message, block_input, unblock_input) class Command(Enum): @@ -93,37 +93,38 @@ class WebAPP(object): if autofill_type == "basic": self._steps = self._default_custom_steps() elif autofill_type == "script": - steps = sorted(self.asset.specific.script, key=lambda step_item: step_item['step']) + script_list = self.asset.specific.script + steps = sorted(script_list, key=lambda step_item: step_item.step) for item in steps: - val = item['value'] + val = item.value if val: val = val.replace("{USERNAME}", self.account.username) val = val.replace("{SECRET}", self.account.secret) - item['value'] = val + item.value = val self._steps.append(item) def _default_custom_steps(self) -> list: account = self.account specific_property = self.asset.specific default_steps = [ - { + Step({ "step": 1, "value": account.username, "target": specific_property.username_selector, "command": "type" - }, - { + }), + Step({ "step": 2, "value": account.secret, "target": specific_property.password_selector, "command": "type" - }, - { + }), + Step({ "step": 3, "value": "", "target": specific_property.submit_selector, "command": "click" - } + }) ] return default_steps @@ -132,7 +133,8 @@ class WebAPP(object): return True for step in self._steps: - action = StepAction(**step) + action = StepAction(target=step.target, value=step.value, + command=step.command) ret = execute_action(driver, action) if not ret: unblock_input() @@ -180,7 +182,9 @@ class AppletApplication(BaseApplication): self.driver.maximize_window() def wait(self): - msg = "Unable to evaluate script: disconnected: not connected to DevTools\n" + disconnected_msg = "Unable to evaluate script: disconnected: not connected to DevTools\n" + closed_msg = "Unable to evaluate script: no such window: target window already closed" + while True: time.sleep(5) logs = self.driver.get_log('driver') @@ -188,14 +192,16 @@ class AppletApplication(BaseApplication): continue ret = logs[-1] if isinstance(ret, dict): - if ret.get("message") == msg: - print(ret) + message = ret.get('message', '') + if disconnected_msg in message or closed_msg in message: break + print("ret: ", ret) self.close() def close(self): if self.driver: try: - self.driver.close() + # quit 退出全部打开的窗口 + self.driver.quit() except Exception as e: print(e) diff --git a/apps/terminal/applets/chrome/common.py b/apps/terminal/applets/chrome/common.py index 75e59a4c1..4bffa5081 100644 --- a/apps/terminal/applets/chrome/common.py +++ b/apps/terminal/applets/chrome/common.py @@ -1,10 +1,11 @@ import abc +import base64 +import json +import locale +import os import subprocess import sys import time -import os -import json -import base64 from subprocess import CREATE_NO_WINDOW _blockInput = None @@ -38,6 +39,16 @@ def notify_err_message(msg): _messageBox(msg, 'Error') +def decode_content(content: bytes) -> str: + for encoding_name in ['utf-8', 'gbk', 'gb2312']: + try: + return content.decode(encoding_name) + except Exception as e: + print(e) + encoding_name = locale.getpreferredencoding() + return content.decode(encoding_name) + + def check_pid_alive(pid) -> bool: # tasklist /fi "PID eq 508" /fo csv # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n' @@ -45,7 +56,7 @@ def check_pid_alive(pid) -> bool: csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"], creationflags=CREATE_NO_WINDOW) - content = csv_ret.decode() + content = decode_content(csv_ret) content_list = content.strip().split("\r\n") if len(content_list) != 2: notify_err_message(content) @@ -82,13 +93,20 @@ class User(DictObj): username: str +class Step(DictObj): + step: int + target: str + command: str + value: str + + class Specific(DictObj): # web autofill: str username_selector: str password_selector: str submit_selector: str - script: list + script: list[Step] # database db_name: str diff --git a/apps/terminal/applets/navicat/README.md b/apps/terminal/applets/navicat/README.md new file mode 100644 index 000000000..b46ffb9e5 --- /dev/null +++ b/apps/terminal/applets/navicat/README.md @@ -0,0 +1,5 @@ + +## Navicat Premium + +- 需要先手动导入License激活 + diff --git a/apps/terminal/applets/navicat/app.py b/apps/terminal/applets/navicat/app.py new file mode 100644 index 000000000..33d89d2d2 --- /dev/null +++ b/apps/terminal/applets/navicat/app.py @@ -0,0 +1,216 @@ +import sys +import time + +if sys.platform == 'win32': + import winreg + import win32api + + from pywinauto import Application + from pywinauto.controls.uia_controls import ( + EditWrapper, ComboBoxWrapper, ButtonWrapper + ) +from common import wait_pid, BaseApplication + + +_default_path = r'C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe' + + +class AppletApplication(BaseApplication): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.path = _default_path + self.username = self.account.username + self.password = self.account.secret + self.privileged = self.account.privileged + self.host = self.asset.address + self.port = self.asset.get_protocol_port(self.protocol) + self.db = self.asset.specific.db_name + self.name = '%s-%s' % (self.host, self.db) + self.pid = None + self.app = None + + def clean_up(self): + protocol_mapping = { + 'mariadb': 'NavicatMARIADB', 'mongodb': 'NavicatMONGODB', + 'mysql': 'Navicat', 'oracle': 'NavicatORA', + 'sqlserver': 'NavicatMSSQL', 'postgresql': 'NavicatPG' + } + protocol_display = protocol_mapping.get(self.protocol, 'mysql') + sub_key = r'Software\PremiumSoft\%s\Servers' % protocol_display + try: + win32api.RegDeleteTree(winreg.HKEY_CURRENT_USER, sub_key) + except Exception as err: + print('Error: %s' % err) + + @staticmethod + def launch(): + sub_key = r'Software\PremiumSoft\NavicatPremium' + try: + key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, sub_key) + # 禁止弹出欢迎页面 + winreg.SetValueEx(key, 'AlreadyShowNavicatV16WelcomeScreen', 0, winreg.REG_DWORD, 1) + # 禁止开启自动检查更新 + winreg.SetValueEx(key, 'AutoCheckUpdate', 0, winreg.REG_DWORD, 0) + winreg.SetValueEx(key, 'ShareUsageData', 0, winreg.REG_DWORD, 0) + except Exception as err: + print('Launch error: %s' % err) + + def _fill_to_mysql(self, app, menu, protocol_display='MySQL'): + menu.item_by_path('File->New Connection->%s' % protocol_display).click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + username_ele = conn_window.child_window(best_match='Edit1') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_mariadb(self, app, menu): + self._fill_to_mysql(app, menu, 'MariaDB') + + def _fill_to_mongodb(self, app, menu): + menu.item_by_path('File->New Connection->MongoDB').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + auth_type_ele = conn_window.child_window(best_match='ComboBox2') + ComboBoxWrapper(auth_type_ele.element_info).select('Password') + + name_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + db_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit1') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_postgresql(self, app, menu): + menu.item_by_path('File->New Connection->PostgreSQL').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + db_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit1') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_sqlserver(self, app, menu): + menu.item_by_path('File->New Connection->SQL Server').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(host_ele.element_info).set_edit_text('%s,%s' % (self.host, self.port)) + + db_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_oracle(self, app, menu): + menu.item_by_path('File->New Connection->Oracle').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + db_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + if self.privileged: + conn_window.child_window(best_match='Advanced', control_type='TabItem').click_input() + role_ele = conn_window.child_window(best_match='ComboBox2') + ComboBoxWrapper(role_ele.element_info).select('SYSDBA') + + def run(self): + self.launch() + app = Application(backend='uia') + app.start(self.path) + self.pid = app.process + + # 检测是否为试用版本 + try: + trial_btn = app.top_window().child_window( + best_match='Trial', control_type='Button' + ) + ButtonWrapper(trial_btn.element_info).click() + time.sleep(0.5) + except Exception: + pass + + menubar = app.window(best_match='Navicat Premium', control_type='Window') \ + .child_window(best_match='Menu', control_type='MenuBar') + + file = menubar.child_window(best_match='File', control_type='MenuItem') + file.click_input() + menubar.item_by_path('File->New Connection').click_input() + + # 根据协议选择动作 + action = getattr(self, '_fill_to_%s' % self.protocol, None) + if action is None: + raise ValueError('This protocol is not supported: %s' % self.protocol) + action(app, menubar) + + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + ok_btn = conn_window.child_window(best_match='OK', control_type='Button') + ok_btn.click() + + file.click_input() + menubar.item_by_path('File->Open Connection').click_input() + self.app = app + + def wait(self): + try: + wait_pid(self.pid) + except Exception: + pass + finally: + self.clean_up() diff --git a/apps/terminal/applets/navicat/common.py b/apps/terminal/applets/navicat/common.py new file mode 100644 index 000000000..f1e6429de --- /dev/null +++ b/apps/terminal/applets/navicat/common.py @@ -0,0 +1,212 @@ +import abc +import subprocess +import locale +import sys +import time +import os +import json +import base64 +from subprocess import CREATE_NO_WINDOW + +_blockInput = None +_messageBox = None +if sys.platform == 'win32': + import ctypes + from ctypes import wintypes + import win32ui + + # import win32con + + _messageBox = win32ui.MessageBox + + _blockInput = ctypes.windll.user32.BlockInput + _blockInput.argtypes = [wintypes.BOOL] + _blockInput.restype = wintypes.BOOL + + +def block_input(): + if _blockInput: + _blockInput(True) + + +def unblock_input(): + if _blockInput: + _blockInput(False) + + +def decode_content(content: bytes) -> str: + for encoding_name in ['utf-8', 'gbk', 'gb2312']: + try: + return content.decode(encoding_name) + except Exception as e: + print(e) + encoding_name = locale.getpreferredencoding() + return content.decode(encoding_name) + + +def notify_err_message(msg): + if _messageBox: + _messageBox(msg, 'Error') + + +def check_pid_alive(pid) -> bool: + # tasklist /fi "PID eq 508" /fo csv + # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n' + try: + + csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"], + creationflags=CREATE_NO_WINDOW) + content = decode_content(csv_ret) + content_list = content.strip().split("\r\n") + if len(content_list) != 2: + notify_err_message(content) + time.sleep(2) + return False + ret_pid = content_list[1].split(",")[1].strip('"') + return str(pid) == ret_pid + except Exception as e: + notify_err_message(e) + time.sleep(2) + return False + + +def wait_pid(pid): + while 1: + time.sleep(5) + ok = check_pid_alive(pid) + if not ok: + notify_err_message("程序退出") + time.sleep(2) + break + + +class DictObj: + def __init__(self, in_dict: dict): + assert isinstance(in_dict, dict) + for key, val in in_dict.items(): + if isinstance(val, (list, tuple)): + setattr(self, key, [DictObj(x) if isinstance(x, dict) else x for x in val]) + else: + setattr(self, key, DictObj(val) if isinstance(val, dict) else val) + + +class User(DictObj): + id: str + name: str + username: str + + +class Specific(DictObj): + # web + autofill: str + username_selector: str + password_selector: str + submit_selector: str + script: list + + # database + db_name: str + + +class Category(DictObj): + value: str + label: str + + +class Protocol(DictObj): + id: str + name: str + port: int + + +class Asset(DictObj): + id: str + name: str + address: str + protocols: list[Protocol] + category: Category + specific: Specific + + def get_protocol_port(self, protocol): + for item in self.protocols: + if item.name == protocol: + return item.port + return None + + +class LabelValue(DictObj): + label: str + value: str + + +class Account(DictObj): + id: str + name: str + username: str + secret: str + privileged: bool + secret_type: LabelValue + + +class Platform(DictObj): + charset: str + name: str + charset: LabelValue + type: LabelValue + + +class Manifest(DictObj): + name: str + version: str + path: str + exec_type: str + connect_type: str + protocols: list[str] + + +def get_manifest_data() -> dict: + current_dir = os.path.dirname(__file__) + manifest_file = os.path.join(current_dir, 'manifest.json') + try: + with open(manifest_file, "r", encoding='utf8') as f: + return json.load(f) + except Exception as e: + print(e) + return {} + + +def read_app_manifest(app_dir) -> dict: + main_json_file = os.path.join(app_dir, "manifest.json") + if not os.path.exists(main_json_file): + return {} + with open(main_json_file, 'r', encoding='utf8') as f: + return json.load(f) + + +def convert_base64_to_dict(base64_str: str) -> dict: + try: + data_json = base64.decodebytes(base64_str.encode('utf-8')).decode('utf-8') + return json.loads(data_json) + except Exception as e: + print(e) + return {} + + +class BaseApplication(abc.ABC): + + def __init__(self, *args, **kwargs): + self.app_name = kwargs.get('app_name', '') + self.protocol = kwargs.get('protocol', '') + self.manifest = Manifest(kwargs.get('manifest', {})) + self.user = User(kwargs.get('user', {})) + self.asset = Asset(kwargs.get('asset', {})) + self.account = Account(kwargs.get('account', {})) + self.platform = Platform(kwargs.get('platform', {})) + + @abc.abstractmethod + def run(self): + raise NotImplementedError('run') + + @abc.abstractmethod + def wait(self): + raise NotImplementedError('wait') diff --git a/apps/terminal/applets/navicat/i18n.yml b/apps/terminal/applets/navicat/i18n.yml new file mode 100644 index 000000000..ec6427048 --- /dev/null +++ b/apps/terminal/applets/navicat/i18n.yml @@ -0,0 +1,3 @@ +- zh: + display_name: Navicat premium 16 + comment: 数据库管理软件 diff --git a/apps/terminal/applets/navicat/icon.png b/apps/terminal/applets/navicat/icon.png new file mode 100644 index 000000000..10b343bf0 Binary files /dev/null and b/apps/terminal/applets/navicat/icon.png differ diff --git a/apps/terminal/applets/navicat/main.py b/apps/terminal/applets/navicat/main.py new file mode 100644 index 000000000..be0ff3585 --- /dev/null +++ b/apps/terminal/applets/navicat/main.py @@ -0,0 +1,22 @@ +import sys + +from common import (block_input, unblock_input) +from common import convert_base64_to_dict +from app import AppletApplication + + +def main(): + base64_str = sys.argv[1] + data = convert_base64_to_dict(base64_str) + applet_app = AppletApplication(**data) + block_input() + applet_app.run() + unblock_input() + applet_app.wait() + + +if __name__ == '__main__': + try: + main() + except Exception as e: + print(e) diff --git a/apps/terminal/applets/navicat/manifest.yml b/apps/terminal/applets/navicat/manifest.yml new file mode 100644 index 000000000..02db9576a --- /dev/null +++ b/apps/terminal/applets/navicat/manifest.yml @@ -0,0 +1,17 @@ +name: navicat +display_name: Navicat premium 16 +comment: Database management software +version: 0.1 +exec_type: python +author: JumpServer Team +type: general +update_policy: always +tags: + - database +protocols: + - mysql + - mariadb + - postgresql + - sqlserver + - oracle + - mongodb diff --git a/apps/terminal/applets/navicat/setup.yml b/apps/terminal/applets/navicat/setup.yml new file mode 100644 index 000000000..60e8d6c43 --- /dev/null +++ b/apps/terminal/applets/navicat/setup.yml @@ -0,0 +1,6 @@ +type: manual # exe, zip, manual +# 从这里下载的: https://www.navicat.com.cn/download/direct-download?product=navicat_premium_en_x64.exe +source: +destination: C:\Program Files\PremiumSoft\Navicat Premium 16 +program: C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe +md5: 6c2c25fa56c75254c6bbcba043000063 diff --git a/apps/terminal/backends/command/serializers.py b/apps/terminal/backends/command/serializers.py index e799f92f2..63920a846 100644 --- a/apps/terminal/backends/command/serializers.py +++ b/apps/terminal/backends/command/serializers.py @@ -17,7 +17,9 @@ class SimpleSessionCommandSerializer(serializers.Serializer): risk_level = serializers.ChoiceField( required=False, label=_("Risk level"), choices=AbstractSessionCommand.RISK_LEVEL_CHOICES ) - org_id = serializers.CharField(max_length=36, required=False, default='', allow_null=True, allow_blank=True) + org_id = serializers.CharField( + max_length=36, required=False, default='', allow_null=True, allow_blank=True + ) def validate_user(self, value): if len(value) > 64: @@ -29,9 +31,8 @@ class InsecureCommandAlertSerializer(SimpleSessionCommandSerializer): pass -class SessionCommandSerializer(SimpleSessionCommandSerializer): +class SessionCommandSerializerMixin(serializers.Serializer): """使用这个类作为基础Command Log Serializer类, 用来序列化""" - id = serializers.UUIDField(read_only=True) # 限制 64 字符,不能直接迁移成 128 字符,命令表数据量会比较大 account = serializers.CharField(label=_("Account ")) @@ -44,3 +45,9 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer): if len(value) > 64: value = pretty_string(value, 64) return value + + +class SessionCommandSerializer(SessionCommandSerializerMixin, SimpleSessionCommandSerializer): + """ 字段排序序列类 """ + pass + diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py index 454abb522..ab7c34fb1 100644 --- a/apps/terminal/connect_methods.py +++ b/apps/terminal/connect_methods.py @@ -30,6 +30,7 @@ class WebMethod(TextChoices): Protocol.sqlserver: [cls.web_cli, cls.web_gui], Protocol.redis: [cls.web_cli], Protocol.mongodb: [cls.web_cli], + Protocol.clickhouse: [cls.web_cli], Protocol.k8s: [cls.web_cli], Protocol.http: [] diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 69f994476..a38d026b4 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -52,3 +52,10 @@ class TerminalType(TextChoices): @classmethod def types(cls): return set(dict(cls.choices).keys()) + + +class PublishStatus(TextChoices): + pending = 'pending', _('Pending') + success = 'success', _("Success") + failed = 'failed', _("Failed") + mismatch = 'mismatch', _("Mismatch") diff --git a/apps/terminal/migrations/0001_initial.py b/apps/terminal/migrations/0001_initial.py index fe604fc01..83d0fb1bc 100644 --- a/apps/terminal/migrations/0001_initial.py +++ b/apps/terminal/migrations/0001_initial.py @@ -2,12 +2,12 @@ # Generated by Django 1.11 on 2017-12-24 15:21 from __future__ import unicode_literals -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): - initial = True dependencies = [ @@ -38,7 +38,8 @@ class Migration(migrations.Migration): ('user', models.CharField(max_length=128, verbose_name='User')), ('asset', models.CharField(max_length=1024, verbose_name='Asset')), ('system_user', models.CharField(max_length=128, verbose_name='System user')), - ('login_from', models.CharField(choices=[('ST', 'SSH Terminal'), ('WT', 'Web Terminal')], default='ST', max_length=2)), + ('login_from', models.CharField(choices=[('ST', 'SSH Terminal'), ('WT', 'Web Terminal')], default='ST', + max_length=2)), ('is_finished', models.BooleanField(default=False)), ('has_replay', models.BooleanField(default=False, verbose_name='Replay')), ('has_command', models.BooleanField(default=False, verbose_name='Command')), @@ -71,7 +72,8 @@ class Migration(migrations.Migration): name='Task', fields=[ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(choices=[('kill_session', 'Kill Session')], max_length=128, verbose_name='Name')), + ('name', + models.CharField(choices=[('kill_session', 'Kill Session')], max_length=128, verbose_name='Name')), ('args', models.CharField(max_length=1024, verbose_name='Args')), ('is_finished', models.BooleanField(default=False)), ('date_created', models.DateTimeField(auto_now_add=True)), @@ -87,8 +89,8 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), ('remote_addr', models.CharField(max_length=128, verbose_name='Remote Address')), - ('ssh_port', models.IntegerField(default=2222, verbose_name='SSH Port')), - ('http_port', models.IntegerField(default=5000, verbose_name='HTTP Port')), + ('ssh_port', models.IntegerField(default=2222, verbose_name='SSH port')), + ('http_port', models.IntegerField(default=5000, verbose_name='HTTP port')), ('is_accepted', models.BooleanField(default=False, verbose_name='Is Accepted')), ('is_deleted', models.BooleanField(default=False)), ('date_created', models.DateTimeField(auto_now_add=True)), diff --git a/apps/terminal/migrations/0048_endpoint_endpointrule.py b/apps/terminal/migrations/0048_endpoint_endpointrule.py index 1ea7c3d03..1c1ef2d8c 100644 --- a/apps/terminal/migrations/0048_endpoint_endpointrule.py +++ b/apps/terminal/migrations/0048_endpoint_endpointrule.py @@ -82,13 +82,13 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('host', models.CharField(max_length=256, verbose_name='Host', blank=True)), - ('https_port', common.db.fields.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS Port')), - ('http_port', common.db.fields.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP Port')), - ('ssh_port', common.db.fields.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH Port')), - ('rdp_port', common.db.fields.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP Port')), - ('mysql_port', common.db.fields.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL Port')), - ('mariadb_port', common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB Port')), - ('postgresql_port', common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL Port')), + ('https_port', common.db.fields.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS port')), + ('http_port', common.db.fields.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP port')), + ('ssh_port', common.db.fields.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH port')), + ('rdp_port', common.db.fields.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP port')), + ('mysql_port', common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL port')), + ('mariadb_port', common.db.fields.PortField(default=33062, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB port')), + ('postgresql_port', common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL port')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ], options={ diff --git a/apps/terminal/migrations/0049_endpoint_redis_port.py b/apps/terminal/migrations/0049_endpoint_redis_port.py index d1b1b38f1..aa4d722bc 100644 --- a/apps/terminal/migrations/0049_endpoint_redis_port.py +++ b/apps/terminal/migrations/0049_endpoint_redis_port.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='endpoint', name='redis_port', - field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis Port'), + field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis port'), ), ] diff --git a/apps/terminal/migrations/0052_auto_20220713_1417.py b/apps/terminal/migrations/0052_auto_20220713_1417.py index c30032c23..52c5d907f 100644 --- a/apps/terminal/migrations/0052_auto_20220713_1417.py +++ b/apps/terminal/migrations/0052_auto_20220713_1417.py @@ -16,13 +16,13 @@ class Migration(migrations.Migration): name='oracle_11g_port', field=common.db.fields.PortField(default=15211, validators=[ django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g Port'), + django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g port'), ), migrations.AddField( model_name='endpoint', name='oracle_12c_port', field=common.db.fields.PortField(default=15212, validators=[ django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c Port'), + django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c port'), ), ] diff --git a/apps/terminal/migrations/0054_auto_20221027_1125.py b/apps/terminal/migrations/0054_auto_20221027_1125.py index 7f5527b58..4b101aea0 100644 --- a/apps/terminal/migrations/0054_auto_20221027_1125.py +++ b/apps/terminal/migrations/0054_auto_20221027_1125.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('assets', '0107_auto_20221019_1115'), + ('assets', '0101_auto_20220811_1511'), ('terminal', '0053_auto_20221009_1755'), ] @@ -32,84 +32,14 @@ class Migration(migrations.Migration): name='account', field=models.CharField(db_index=True, max_length=128, verbose_name='Account'), ), - migrations.CreateModel( - name='Applet', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), - ('display_name', models.CharField(max_length=128, verbose_name='Display name')), - ('version', models.CharField(max_length=16, verbose_name='Version')), - ('author', models.CharField(max_length=128, verbose_name='Author')), - ('type', - models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, - verbose_name='Type')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active')), - ('protocols', models.JSONField(default=list, verbose_name='Protocol')), - ('tags', models.JSONField(default=list, verbose_name='Tags')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='AppletHost', - fields=[ - ('host_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.host')), - ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), - ('status', models.CharField(max_length=16, verbose_name='Status')), - ], - options={ - 'abstract': False, - }, - bases=('assets.host',), - ), - migrations.CreateModel( - name='AppletPublication', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='', max_length=16, verbose_name='Status')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applet', - verbose_name='Applet')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applethost', - verbose_name='Host')), - ], - options={ - 'unique_together': {('applet', 'host')}, - }, - ), - migrations.CreateModel( - name='AppletHostDeployment', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(max_length=16, default='', verbose_name='Status')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', - verbose_name='Hosting')), - ], - options={ - 'abstract': False, - }, + migrations.AddField( + model_name='session', + name='comment', + field=models.TextField(blank=True, null=True, verbose_name='Comment'), ), migrations.AddField( - model_name='applethost', - name='applets', - field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', - verbose_name='Applet'), + model_name='session', + name='type', + field=models.CharField(db_index=True, default='normal', max_length=16), ), ] diff --git a/apps/terminal/migrations/0055_auto_20221031_1848.py b/apps/terminal/migrations/0055_auto_20221031_1848.py deleted file mode 100644 index e36ed5a9b..000000000 --- a/apps/terminal/migrations/0055_auto_20221031_1848.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-31 10:48 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0054_auto_20221027_1125'), - ] - - operations = [ - migrations.AddField( - model_name='applet', - name='hosts', - field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.AppletHost', verbose_name='Hosts'), - ), - migrations.AddField( - model_name='applethost', - name='date_inited', - field=models.DateTimeField(blank=True, null=True, verbose_name='Date inited'), - ), - migrations.AddField( - model_name='applethost', - name='inited', - field=models.BooleanField(default=False, verbose_name='Inited'), - ), - migrations.AddField( - model_name='applethostdeployment', - name='date_finished', - field=models.DateTimeField(null=True, verbose_name='Date finished'), - ), - migrations.AddField( - model_name='applethostdeployment', - name='date_start', - field=models.DateTimeField(db_index=True, null=True, verbose_name='Date start'), - ), - migrations.AlterField( - model_name='appletpublication', - name='applet', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applet', verbose_name='Applet'), - ), - migrations.AlterField( - model_name='appletpublication', - name='host', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'), - ), - ] diff --git a/apps/terminal/migrations/0064_auto_20221220_1956.py b/apps/terminal/migrations/0055_auto_20221228_1804.py similarity index 81% rename from apps/terminal/migrations/0064_auto_20221220_1956.py rename to apps/terminal/migrations/0055_auto_20221228_1804.py index 93b47dedc..126cbdbbf 100644 --- a/apps/terminal/migrations/0064_auto_20221220_1956.py +++ b/apps/terminal/migrations/0055_auto_20221228_1804.py @@ -1,15 +1,43 @@ -# Generated by Django 3.2.14 on 2022-12-20 11:56 +# Generated by Django 3.2.14 on 2022-12-28 10:04 from django.db import migrations, models +import django.db.models.deletion +import uuid class Migration(migrations.Migration): - dependencies = [ - ('terminal', '0063_applet_builtin'), + ('assets', '0105_auto_20221220_1956'), + ('terminal', '0054_auto_20221027_1125'), ] operations = [ + migrations.AlterModelOptions( + name='terminal', + options={'permissions': (('view_terminalconfig', 'Can view terminal config'),), 'verbose_name': 'Terminal'}, + ), + migrations.RenameField( + model_name='command', + old_name='system_user', + new_name='account', + ), + migrations.AlterField( + model_name='command', + name='account', + field=models.CharField(db_index=True, max_length=64, verbose_name='Account'), + ), + migrations.RemoveField( + model_name='terminal', + name='http_port', + ), + migrations.RemoveField( + model_name='terminal', + name='is_accepted', + ), + migrations.RemoveField( + model_name='terminal', + name='ssh_port', + ), migrations.AddField( model_name='commandstorage', name='updated_by', @@ -85,36 +113,6 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='applet', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='applet', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='applethostdeployment', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='applethostdeployment', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='appletpublication', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='appletpublication', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), migrations.AlterField( model_name='commandstorage', name='created_by', diff --git a/apps/terminal/migrations/0056_auto_20221101_1353.py b/apps/terminal/migrations/0056_auto_20221101_1353.py deleted file mode 100644 index 798420e2c..000000000 --- a/apps/terminal/migrations/0056_auto_20221101_1353.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-01 05:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0055_auto_20221031_1848'), - ] - - operations = [ - migrations.AddField( - model_name='applethost', - name='deploy_options', - field=models.JSONField(default=dict, verbose_name='Deploy options'), - ), - migrations.AddField( - model_name='applethostdeployment', - name='initial', - field=models.BooleanField(default=False, verbose_name='Initial'), - ), - ] diff --git a/apps/terminal/migrations/0056_auto_20221228_1808.py b/apps/terminal/migrations/0056_auto_20221228_1808.py new file mode 100644 index 000000000..c4b5b8a14 --- /dev/null +++ b/apps/terminal/migrations/0056_auto_20221228_1808.py @@ -0,0 +1,125 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:08 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0105_auto_20221220_1956'), + ('terminal', '0055_auto_20221228_1804'), + ] + + operations = [ + migrations.CreateModel( + name='Applet', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), + ('display_name', models.CharField(max_length=128, verbose_name='Display name')), + ('version', models.CharField(max_length=16, verbose_name='Version')), + ('author', models.CharField(max_length=128, verbose_name='Author')), + ('type', + models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, + verbose_name='Type')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('builtin', models.BooleanField(default=False, verbose_name='Builtin')), + ('protocols', models.JSONField(default=list, verbose_name='Protocol')), + ('tags', models.JSONField(default=list, verbose_name='Tags')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ], + options={ + 'verbose_name': 'Applet', + }, + ), + migrations.CreateModel( + name='AppletHost', + fields=[ + ('host_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.host')), + ('deploy_options', models.JSONField(default=dict, verbose_name='Deploy options')), + ('inited', models.BooleanField(default=False, verbose_name='Inited')), + ('date_inited', models.DateTimeField(blank=True, null=True, verbose_name='Date inited')), + ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), + ], + options={ + 'verbose_name': 'Applet host', + }, + bases=('assets.host',), + ), + migrations.CreateModel( + name='AppletPublication', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', + to='terminal.applet', verbose_name='Applet')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', + to='terminal.applethost', verbose_name='Host')), + ], + options={ + 'unique_together': {('applet', 'host')}, + }, + ), + migrations.CreateModel( + name='AppletHostDeployment', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('initial', models.BooleanField(default=False, verbose_name='Initial')), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('task', models.UUIDField(null=True, verbose_name='Task')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', + verbose_name='Hosting')), + ], + options={ + 'ordering': ('-date_start',), + }, + ), + migrations.AddField( + model_name='applethost', + name='applets', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', + verbose_name='Applet'), + ), + migrations.AddField( + model_name='applethost', + name='terminal', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='applet_host', to='terminal.terminal', verbose_name='Terminal'), + ), + migrations.AddField( + model_name='applet', + name='hosts', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.AppletHost', + verbose_name='Hosts'), + ), + migrations.AlterField( + model_name='appletpublication', + name='applet', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applet', verbose_name='Applet'), + ), + migrations.AlterField( + model_name='appletpublication', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applethost', verbose_name='Host'), + ), + ] diff --git a/apps/terminal/migrations/0057_auto_20221102_1941.py b/apps/terminal/migrations/0057_auto_20221102_1941.py deleted file mode 100644 index 56f1e699a..000000000 --- a/apps/terminal/migrations/0057_auto_20221102_1941.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-02 11:41 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0056_auto_20221101_1353'), - ] - - operations = [ - migrations.AddField( - model_name='applethost', - name='terminal', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='applet_host', to='terminal.terminal', verbose_name='Terminal'), - ), - migrations.AlterField( - model_name='appletpublication', - name='status', - field=models.CharField(default='ready', max_length=16, verbose_name='Status'), - ), - ] diff --git a/apps/terminal/migrations/0057_auto_20230109_1447.py b/apps/terminal/migrations/0057_auto_20230109_1447.py new file mode 100644 index 000000000..6bb719828 --- /dev/null +++ b/apps/terminal/migrations/0057_auto_20230109_1447.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.14 on 2023-01-09 06:47 + +import common.db.fields +import django.core.validators +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0056_auto_20221228_1808'), + ] + + operations = [ + migrations.AddField( + model_name='endpoint', + name='mariadb_port', + field=common.db.fields.PortField(default=33062, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB port'), + ), + migrations.AddField( + model_name='endpoint', + name='mysql_port', + field=common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL port'), + ), + migrations.AddField( + model_name='endpoint', + name='oracle_port_range', + field=common.db.fields.PortRangeField(default='1-65535', max_length=16, verbose_name='Oracle port range'), + ), + migrations.AddField( + model_name='endpoint', + name='postgresql_port', + field=common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL port'), + ), + migrations.AddField( + model_name='endpoint', + name='redis_port', + field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis port'), + ), + ] diff --git a/apps/terminal/migrations/0058_auto_20221103_1624.py b/apps/terminal/migrations/0058_auto_20221103_1624.py deleted file mode 100644 index 0c8091e5c..000000000 --- a/apps/terminal/migrations/0058_auto_20221103_1624.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-03 08:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0057_auto_20221102_1941'), - ] - - operations = [ - migrations.AlterModelOptions( - name='terminal', - options={'permissions': (('view_terminalconfig', 'Can view terminal config'),), 'verbose_name': 'Terminal'}, - ), - migrations.RemoveField( - model_name='terminal', - name='http_port', - ), - migrations.RemoveField( - model_name='terminal', - name='is_accepted', - ), - migrations.RemoveField( - model_name='terminal', - name='ssh_port', - ), - migrations.RemoveField( - model_name='applethost', - name='status', - ), - ] diff --git a/apps/terminal/migrations/0058_auto_20230110_1445.py b/apps/terminal/migrations/0058_auto_20230110_1445.py new file mode 100644 index 000000000..1c6d87fbf --- /dev/null +++ b/apps/terminal/migrations/0058_auto_20230110_1445.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0057_auto_20230109_1447'), + ] + + operations = [ + migrations.AlterModelOptions( + name='applethost', + options={'verbose_name': 'Hosting'}, + ), + migrations.RemoveField( + model_name='endpoint', + name='oracle_port_range', + ), + migrations.AlterField( + model_name='appletpublication', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applethost', verbose_name='Hosting'), + ), + ] diff --git a/apps/terminal/migrations/0059_applethostdeployment_task.py b/apps/terminal/migrations/0059_applethostdeployment_task.py deleted file mode 100644 index 5f455c9c6..000000000 --- a/apps/terminal/migrations/0059_applethostdeployment_task.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-15 05:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0058_auto_20221103_1624'), - ] - - operations = [ - migrations.AddField( - model_name='applethostdeployment', - name='task', - field=models.UUIDField(null=True, verbose_name='Task'), - ), - ] diff --git a/apps/terminal/migrations/0060_alter_applethostdeployment_options.py b/apps/terminal/migrations/0060_alter_applethostdeployment_options.py deleted file mode 100644 index c38e2ba29..000000000 --- a/apps/terminal/migrations/0060_alter_applethostdeployment_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-18 02:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0059_applethostdeployment_task'), - ] - - operations = [ - migrations.AlterModelOptions( - name='applethostdeployment', - options={'ordering': ('-date_start',)}, - ), - ] diff --git a/apps/terminal/migrations/0061_rename_system_user_command_account.py b/apps/terminal/migrations/0061_rename_system_user_command_account.py deleted file mode 100644 index ab7ee1f32..000000000 --- a/apps/terminal/migrations/0061_rename_system_user_command_account.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 05:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0060_alter_applethostdeployment_options'), - ] - - operations = [ - migrations.RenameField( - model_name='command', - old_name='system_user', - new_name='account', - ), - migrations.AlterField( - model_name='command', - name='account', - field=models.CharField(db_index=True, max_length=64, verbose_name='Account'), - ), - ] diff --git a/apps/terminal/migrations/0062_auto_20221216_1529.py b/apps/terminal/migrations/0062_auto_20221216_1529.py deleted file mode 100644 index 295cc8b55..000000000 --- a/apps/terminal/migrations/0062_auto_20221216_1529.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-16 07:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0061_rename_system_user_command_account'), - ] - - operations = [ - migrations.AddField( - model_name='session', - name='comment', - field=models.TextField(blank=True, null=True, verbose_name='Comment'), - ), - migrations.AddField( - model_name='session', - name='type', - field=models.CharField(db_index=True, default='normal', max_length=16), - ), - ] diff --git a/apps/terminal/migrations/0065_auto_20221223_1536.py b/apps/terminal/migrations/0065_auto_20221223_1536.py deleted file mode 100644 index 23b070be8..000000000 --- a/apps/terminal/migrations/0065_auto_20221223_1536.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-23 07:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0064_auto_20221220_1956'), - ] - - operations = [ - migrations.AlterModelOptions( - name='applet', - options={'verbose_name': 'Applet'}, - ), - migrations.AlterModelOptions( - name='applethost', - options={'verbose_name': 'Applet host'}, - ), - ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index b898cf2ed..6d0d0d3bf 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -1,5 +1,6 @@ import os.path import random +import shutil import yaml from django.conf import settings @@ -87,6 +88,11 @@ class Applet(JMSBaseModel): serializer = AppletSerializer(instance=instance, data=manifest) serializer.is_valid() serializer.save(builtin=True) + pkg_path = default_storage.path('applets/{}'.format(name)) + + if os.path.exists(pkg_path): + shutil.rmtree(pkg_path) + shutil.copytree(path, pkg_path) return instance def select_host_account(self): @@ -123,11 +129,11 @@ class Applet(JMSBaseModel): class AppletPublication(JMSBaseModel): - applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', + applet = models.ForeignKey('Applet', on_delete=models.CASCADE, related_name='publications', verbose_name=_('Applet')) - host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', - verbose_name=_('Host')) - status = models.CharField(max_length=16, default='ready', verbose_name=_('Status')) + host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, related_name='publications', + verbose_name=_('Hosting')) + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) class Meta: diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py index 311fbe61a..37d15e64d 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -30,7 +30,7 @@ class AppletHost(Host): LOCKING_ORG = '00000000-0000-0000-0000-000000000004' class Meta: - verbose_name = _("Applet host") + verbose_name = _('Hosting') def __str__(self): return self.name @@ -104,7 +104,7 @@ class AppletHost(Host): class AppletHostDeployment(JMSBaseModel): host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting')) initial = models.BooleanField(default=False, verbose_name=_('Initial')) - status = models.CharField(max_length=16, default='', verbose_name=_('Status')) + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) diff --git a/apps/terminal/models/component/endpoint.py b/apps/terminal/models/component/endpoint.py index 61d5e06d7..47d7a7894 100644 --- a/apps/terminal/models/component/endpoint.py +++ b/apps/terminal/models/component/endpoint.py @@ -12,10 +12,14 @@ class Endpoint(JMSBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) host = models.CharField(max_length=256, blank=True, verbose_name=_('Host')) # value=0 表示 disabled - https_port = PortField(default=443, verbose_name=_('HTTPS Port')) - http_port = PortField(default=80, verbose_name=_('HTTP Port')) - ssh_port = PortField(default=2222, verbose_name=_('SSH Port')) - rdp_port = PortField(default=3389, verbose_name=_('RDP Port')) + https_port = PortField(default=443, verbose_name=_('HTTPS port')) + http_port = PortField(default=80, verbose_name=_('HTTP port')) + ssh_port = PortField(default=2222, verbose_name=_('SSH port')) + rdp_port = PortField(default=3389, verbose_name=_('RDP port')) + mysql_port = PortField(default=33061, verbose_name=_('MySQL port')) + mariadb_port = PortField(default=33062, verbose_name=_('MariaDB port')) + postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL port')) + redis_port = PortField(default=63790, verbose_name=_('Redis port')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) @@ -30,12 +34,12 @@ class Endpoint(JMSBaseModel): def get_port(self, target_instance, protocol): from terminal.utils import db_port_manager - if protocol in ['https', 'http', 'ssh', 'rdp']: - port = getattr(self, f'{protocol}_port', 0) - elif isinstance(target_instance, Asset) and target_instance.category == 'dabase': + from assets.const import DatabaseTypes + if isinstance(target_instance, Asset) and \ + target_instance.is_type(DatabaseTypes.ORACLE): port = db_port_manager.get_port_by_db(target_instance) else: - port = 0 + port = getattr(self, f'{protocol}_port', 0) return port def is_default(self): diff --git a/apps/terminal/serializers/applet.py b/apps/terminal/serializers/applet.py index 35af7e07b..89fdf19b5 100644 --- a/apps/terminal/serializers/applet.py +++ b/apps/terminal/serializers/applet.py @@ -1,25 +1,20 @@ -from rest_framework import serializers from django.utils.translation import gettext_lazy as _ -from django.db import models +from rest_framework import serializers -from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.const.choices import Status +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from terminal.const import PublishStatus from ..models import Applet, AppletPublication, AppletHost - __all__ = [ 'AppletSerializer', 'AppletPublicationSerializer', ] class AppletPublicationSerializer(serializers.ModelSerializer): - class Status(models.TextChoices): - PUBLISHED = 'published', _('Published') - UNPUBLISHED = 'unpublished', _('Unpublished') - NOT_MATCH = 'not_match', _('Not match') - 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")) + status = LabeledChoiceField(choices=PublishStatus.choices, label=_("Status"), default=Status.pending) class Meta: model = AppletPublication diff --git a/apps/terminal/serializers/applet_host.py b/apps/terminal/serializers/applet_host.py index c81258892..9ec4b8104 100644 --- a/apps/terminal/serializers/applet_host.py +++ b/apps/terminal/serializers/applet_host.py @@ -1,9 +1,11 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.models import Platform, Account +from accounts.models import Account +from assets.models import Platform from assets.serializers import HostSerializer -from common.drf.fields import LabeledChoiceField +from common.const.choices import Status +from common.serializers.fields import LabeledChoiceField from common.validators import ProjectUniqueValidator from .applet import AppletSerializer from .. import const @@ -46,6 +48,7 @@ class AppletHostSerializer(HostSerializer): 'load', 'date_synced', 'deploy_options' ] extra_kwargs = { + **HostSerializer.Meta.extra_kwargs, 'date_synced': {'read_only': True} } @@ -84,6 +87,8 @@ class HostAppletSerializer(AppletSerializer): class AppletHostDeploymentSerializer(serializers.ModelSerializer): + status = LabeledChoiceField(choices=Status.choices, label=_('Status'), default=Status.pending) + class Meta: model = AppletHostDeployment fields_mini = ['id', 'host', 'status', 'task'] diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py index ce45ffa3d..57439c48d 100644 --- a/apps/terminal/serializers/endpoint.py +++ b/apps/terminal/serializers/endpoint.py @@ -1,22 +1,22 @@ -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.drf.serializers import BulkModelSerializer -from acls.serializers.rules import ip_group_child_validator, ip_group_help_text -from ..utils import db_port_manager -from ..models import Endpoint, EndpointRule +from rest_framework import serializers +from acls.serializers.rules import ip_group_child_validator, ip_group_help_text +from common.serializers import BulkModelSerializer +from ..models import Endpoint, EndpointRule +from ..utils import db_port_manager __all__ = ['EndpointSerializer', 'EndpointRuleSerializer'] class EndpointSerializer(BulkModelSerializer): # 解决 luna 处理繁琐的问题, 返回 magnus 监听的当前 db 的 port - magnus_listen_db_port = serializers.SerializerMethodField(label=_('Magnus listen db port')) - magnus_listen_port_range = serializers.CharField( - max_length=128, default=db_port_manager.magnus_listen_port_range, read_only=True, - label=_('Magnus Listen port range'), + oracle_port = serializers.SerializerMethodField(label=_('Oracle port')) + oracle_port_range = serializers.CharField( + max_length=128, default=db_port_manager.oracle_port_range, read_only=True, + label=_('Oracle port range'), help_text=_( - 'The range of ports that Magnus listens on is modified in the configuration file' + 'Oracle proxy server listen port is dynamic, Each additional Oracle database instance adds a port listener' ) ) @@ -24,26 +24,33 @@ class EndpointSerializer(BulkModelSerializer): model = Endpoint fields_mini = ['id', 'name'] fields_small = [ - 'host', - 'https_port', 'http_port', 'ssh_port', 'rdp_port', - 'magnus_listen_db_port', 'magnus_listen_port_range', + 'host', 'https_port', 'http_port', 'ssh_port', 'rdp_port', + 'mysql_port', 'mariadb_port', 'postgresql_port', 'redis_port', + 'oracle_port_range', 'oracle_port', ] fields = fields_mini + fields_small + [ 'comment', 'date_created', 'date_updated', 'created_by' ] extra_kwargs = { - 'https_port': {'default': 443}, - 'http_port': {'default': 80}, - 'ssh_port': {'default': 2222}, - 'rdp_port': {'default': 3389}, + 'host': {'help_text': 'Visit IP/host, if empty, use the current request instead'}, } - def get_magnus_listen_db_port(self, obj: Endpoint): + def get_oracle_port(self, obj: Endpoint): view = self.context.get('view') if not view or view.action not in ['smart']: return 0 return obj.get_port(view.target_instance, view.target_protocol) + def get_extra_kwargs(self): + extra_kwargs = super().get_extra_kwargs() + model_fields = self.Meta.model._meta.fields + for field in model_fields: + if field.name.endswith('_port'): + kwargs = extra_kwargs.get(field.name, {}) + kwargs = {'default': field.default, **kwargs} + extra_kwargs[field.name] = kwargs + return extra_kwargs + class EndpointRuleSerializer(BulkModelSerializer): _ip_group_help_text = '{}
{}'.format( diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 184262ec4..b0494f42a 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.const import Protocol -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Session @@ -22,7 +22,12 @@ class SessionType(models.TextChoices): class SessionSerializer(BulkOrgResourceModelSerializer): org_id = serializers.CharField(allow_blank=True) protocol = serializers.ChoiceField(choices=Protocol.choices, label=_("Protocol")) - type = LabeledChoiceField(choices=SessionType.choices, label=_("Type"), default=SessionType.normal) + type = LabeledChoiceField( + choices=SessionType.choices, label=_("Type"), default=SessionType.normal + ) + can_replay = serializers.BooleanField(read_only=True, label=_("Can replay")) + can_join = serializers.BooleanField(read_only=True, label=_("Can join")) + can_terminate = serializers.BooleanField(read_only=True, label=_("Can terminate")) class Meta: model = Session diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index cc4478c2d..8bc66fd5a 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -5,9 +5,9 @@ from rest_framework.validators import UniqueValidator from urllib.parse import urlparse from django.utils.translation import ugettext_lazy as _ from django.db.models import TextChoices - -from common.drf.serializers import MethodSerializer -from common.drf.fields import ReadableHiddenField, EncryptedField +from common.serializers.fields import LabeledChoiceField +from common.serializers import MethodSerializer +from common.serializers.fields import ReadableHiddenField, EncryptedField from ..models import ReplayStorage, CommandStorage from .. import const @@ -178,6 +178,7 @@ command_storage_type_serializer_classes_mapping = { # BaseStorageSerializer class BaseStorageSerializer(serializers.ModelSerializer): + type = LabeledChoiceField(choices=const.ReplayStorageType.choices, label=_('Type')) storage_type_serializer_classes_mapping = {} meta = MethodSerializer() diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 340a0b0f7..83ab1605b 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -1,8 +1,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField -from common.drf.serializers import BulkModelSerializer +from common.serializers.fields import LabeledChoiceField +from common.serializers import BulkModelSerializer from common.utils import get_request_ip, pretty_string, is_uuid from users.serializers import ServiceAccountSerializer from .. import const diff --git a/apps/terminal/signal_handlers/__init__.py b/apps/terminal/signal_handlers/__init__.py new file mode 100644 index 000000000..265f6416d --- /dev/null +++ b/apps/terminal/signal_handlers/__init__.py @@ -0,0 +1,3 @@ +from .applet import * +from .db_port import * +from .terminal import * diff --git a/apps/terminal/signal_handlers.py b/apps/terminal/signal_handlers/applet.py similarity index 64% rename from apps/terminal/signal_handlers.py rename to apps/terminal/signal_handlers/applet.py index 046c1b122..18595fc7c 100644 --- a/apps/terminal/signal_handlers.py +++ b/apps/terminal/signal_handlers/applet.py @@ -1,21 +1,15 @@ -# -*- coding: utf-8 -*- -# - from django.db.models.signals import post_save, post_delete -from django.db.utils import ProgrammingError from django.dispatch import receiver from django.utils.functional import LazyObject -from assets.models import Asset from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub from orgs.utils import tmp_to_builtin_org -from .models import Applet, AppletHost -from .utils import db_port_manager, DBPortManager +from ..models import Applet, AppletHost +from ..utils import DBPortManager db_port_manager: DBPortManager - logger = get_logger(__file__) @@ -31,6 +25,11 @@ def on_applet_host_create(sender, instance, created=False, **kwargs): applet_host_change_pub_sub.publish(True) +@receiver(post_delete, sender=AppletHost) +def on_applet_host_delete(sender, instance, **kwargs): + applet_host_change_pub_sub.publish(True) + + @receiver(post_save, sender=Applet) def on_applet_create(sender, instance, created=False, **kwargs): if not created: @@ -41,29 +40,9 @@ def on_applet_create(sender, instance, created=False, **kwargs): applet_host_change_pub_sub.publish(True) -@receiver(django_ready) -def check_db_port_mapper(sender, **kwargs): - logger.info('Init db port mapper') - try: - db_port_manager.check() - except (ProgrammingError,) as e: - pass - - -@receiver(post_save, sender=Asset) -def on_db_app_created(sender, instance: Asset, created, **kwargs): - if not instance.category != 'database': - return - if not created: - return - db_port_manager.add(instance) - - -@receiver(post_delete, sender=Asset) -def on_db_app_delete(sender, instance, **kwargs): - if not instance.category != 'database': - return - db_port_manager.pop(instance) +@receiver(post_delete, sender=Applet) +def on_applet_delete(sender, instance, **kwargs): + applet_host_change_pub_sub.publish(True) class AppletHostPubSub(LazyObject): diff --git a/apps/terminal/signal_handlers/db_port.py b/apps/terminal/signal_handlers/db_port.py new file mode 100644 index 000000000..9ee60257c --- /dev/null +++ b/apps/terminal/signal_handlers/db_port.py @@ -0,0 +1,39 @@ +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from assets.models import Asset +from common.decorator import on_transaction_commit +from common.signals import django_ready +from common.utils import get_logger +from ..utils import db_port_manager + +logger = get_logger(__file__) + + +@receiver(django_ready) +def check_db_port_mapper(sender, **kwargs): + logger.info('Check oracle ports') + try: + db_port_manager.check() + except Exception as e: + pass + + +@receiver(post_save, sender=Asset) +@on_transaction_commit +def on_db_created(sender, instance: Asset, created, **kwargs): + if instance.type != 'oracle': + return + if not created: + return + logger.info("Oracle create signal recv: {} {}".format(instance, instance.type)) + db_port_manager.check() + + +@receiver(post_delete, sender=Asset) +@on_transaction_commit +def on_db_delete(sender, instance, **kwargs): + if instance.type != 'oracle': + return + logger.info("Oracle delete signal recv: {}".format(instance)) + db_port_manager.check() diff --git a/apps/terminal/signal_handlers/terminal.py b/apps/terminal/signal_handlers/terminal.py new file mode 100644 index 000000000..10d87b989 --- /dev/null +++ b/apps/terminal/signal_handlers/terminal.py @@ -0,0 +1,35 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.functional import LazyObject + +from common.decorator import on_transaction_commit +from common.utils import get_logger +from common.utils.connection import RedisPubSub +from ..models import Task +from ..utils import DBPortManager + +db_port_manager: DBPortManager + +logger = get_logger(__file__) + + +class ComponentEventChan(LazyObject): + def _setup(self): + self._wrapped = RedisPubSub('fm.component_event_chan') + + +component_event_chan = ComponentEventChan() + + +@receiver(post_save, sender=Task) +@on_transaction_commit +def on_task_created(sender, instance: Task, created, **kwargs): + if not created: + return + event = { + "type": instance.name, + "payload": { + "id": str(instance.id), + }, + } + component_event_chan.publish(event) diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 5d3c373d2..c13892346 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -53,8 +53,4 @@ urlpatterns = [ path('components/connect-methods/', api.ConnectMethodListApi.as_view(), name='connect-methods'), ] -old_version_urlpatterns = [ - re_path('(?Pterminal|command)/.*', capi.redirect_plural_name_api) -] - -urlpatterns += router.urls + old_version_urlpatterns +urlpatterns += router.urls diff --git a/apps/terminal/urls/ws_urls.py b/apps/terminal/urls/ws_urls.py new file mode 100644 index 000000000..e779534ec --- /dev/null +++ b/apps/terminal/urls/ws_urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .. import ws + +app_name = 'terminal' + +urlpatterns = [ + path('ws/terminal-task/', ws.TerminalTaskWebsocket.as_asgi(), name='terminal-task-ws'), +] diff --git a/apps/terminal/utils/db_port_mapper.py b/apps/terminal/utils/db_port_mapper.py index 4ca8cebec..6f8457d22 100644 --- a/apps/terminal/utils/db_port_mapper.py +++ b/apps/terminal/utils/db_port_mapper.py @@ -2,8 +2,8 @@ from django.conf import settings from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ -from assets.const import Category -from assets.models import Asset +from assets.const import DatabaseTypes +from assets.models import Database from common.decorator import Singleton from common.exceptions import JMSException from common.utils import get_logger, get_object_or_none @@ -15,57 +15,64 @@ logger = get_logger(__file__) @Singleton class DBPortManager(object): """ 管理端口-数据库ID的映射, Magnus 要使用 """ - CACHE_KEY = 'PORT_DB_MAPPER' def __init__(self): + oracle_ports = self.oracle_port_range try: - port_start, port_end = settings.MAGNUS_PORTS.split('-') + port_start, port_end = oracle_ports.split('-') port_start, port_end = int(port_start), int(port_end) except Exception as e: - logger.error('MAGNUS_PORTS config error: {}'.format(e)) + logger.error('MAGNUS_ORACLE_PORTS config error: {}'.format(e)) port_start, port_end = 30000, 30100 self.port_start, self.port_end = port_start, port_end # 可以使用的端口列表 - self.all_available_ports = list(range(self.port_start, self.port_end + 1)) + self.all_avail_ports = list(range(self.port_start, self.port_end + 1)) @property - def magnus_listen_port_range(self): - return settings.MAGNUS_PORTS + def oracle_port_range(self): + oracle_ports = settings.MAGNUS_ORACLE_PORTS + if not oracle_ports and settings.MAGNUS_PORTS: + oracle_ports = settings.MAGNUS_PORTS + return oracle_ports @staticmethod def fetch_dbs(): with tmp_to_root_org(): - dbs = Asset.objects.filter(platform__category=Category.DATABASE).order_by('id') + dbs = Database.objects.filter(platform__type=DatabaseTypes.ORACLE).order_by('id') return dbs def check(self): dbs = self.fetch_dbs() - for db in dbs: - port = self.get_port_by_db(db, raise_exception=False) - if not port: - self.add(db) + mapper = self.get_mapper() + db_ids = [str(db.id) for db in dbs] + db_ids_to_add = list(set(db_ids) - set(mapper.values())) + mapper = self.bulk_add(db_ids_to_add, mapper) + + db_ids_to_pop = set(mapper.values()) - set(db_ids) + mapper = self.bulk_pop(db_ids_to_pop, mapper) + self.set_mapper(mapper) + + if settings.DEBUG: + logger.debug("Oracle listen ports: {}".format(len(mapper.keys()))) def init(self): dbs = self.fetch_dbs() db_ids = dbs.values_list('id', flat=True) db_ids = [str(i) for i in db_ids] - mapper = dict(zip(self.all_available_ports, list(db_ids))) + mapper = dict(zip(self.all_avail_ports, list(db_ids))) self.set_mapper(mapper) - def add(self, db: Asset): - mapper = self.get_mapper() - available_port = self.get_next_available_port() - mapper.update({available_port: str(db.id)}) - self.set_mapper(mapper) - return True + def bulk_add(self, db_ids, mapper): + for db_id in db_ids: + avail_port = self.get_next_avail_port(mapper) + mapper[avail_port] = str(db_id) + return mapper - def pop(self, db: Asset): - mapper = self.get_mapper() - to_delete_port = self.get_port_by_db(db, raise_exception=False) - mapper.pop(to_delete_port, None) - self.set_mapper(mapper) + def bulk_pop(self, db_ids, mapper): + new_mapper = {port: str(db_id) for port, db_id in mapper.items() if db_id not in db_ids} + return new_mapper def get_port_by_db(self, db, raise_exception=True): mapper = self.get_mapper() @@ -91,29 +98,31 @@ class DBPortManager(object): if not db_id: raise JMSException('Database not in port-db mapper, port: {}'.format(port)) with tmp_to_root_org(): - db = get_object_or_none(Asset, id=db_id) + db = get_object_or_none(Database, id=db_id) if not db: raise JMSException('Database not exists, db id: {}'.format(db_id)) return db - def get_next_available_port(self): - already_use_ports = self.get_already_use_ports() - available_ports = sorted(list(set(self.all_available_ports) - set(already_use_ports))) - if len(available_ports) <= 0: + def get_next_avail_port(self, mapper=None): + if mapper is None: + mapper = self.get_mapper() + already_use_ports = [int(i) for i in mapper.keys()] + avail_ports = sorted(list(set(self.all_avail_ports) - set(already_use_ports))) + if len(avail_ports) <= 0: msg = _('No ports can be used, check and modify the limit on the number ' 'of ports that Magnus listens on in the configuration file.') tips = _('All available port count: {}, Already use port count: {}').format( - len(self.all_available_ports), len(already_use_ports) + len(self.all_avail_ports), len(already_use_ports) ) error = msg + tips raise JMSException(error) - port = available_ports[0] + port = avail_ports[0] logger.debug('Get next available port: {}'.format(port)) return port def get_already_use_ports(self): mapper = self.get_mapper() - return sorted(list(mapper.keys())) + return sorted([int(i) for i in mapper.keys()]) def get_mapper(self): mapper = cache.get(self.CACHE_KEY, {}) diff --git a/apps/terminal/ws.py b/apps/terminal/ws.py new file mode 100644 index 000000000..c414ecfcf --- /dev/null +++ b/apps/terminal/ws.py @@ -0,0 +1,79 @@ +import datetime + +from channels.generic.websocket import JsonWebsocketConsumer +from django.utils import timezone +from rest_framework.renderers import JSONRenderer + +from common.db.utils import safe_db_connection +from common.utils import get_logger +from common.utils.connection import Subscription +from terminal.models import Terminal +from terminal.models import Session +from terminal.serializers import TaskSerializer, StatSerializer +from .signal_handlers import component_event_chan + +logger = get_logger(__name__) + + +class TerminalTaskWebsocket(JsonWebsocketConsumer): + sub: Subscription = None + terminal: Terminal = None + + def connect(self): + user = self.scope["user"] + if user.is_authenticated and user.terminal: + self.accept() + self.terminal = user.terminal + self.sub = self.watch_component_event() + else: + self.close() + + def receive_json(self, content, **kwargs): + req_type = content.get('type') + if req_type == "status": + payload = content.get('payload') + self.handle_status(payload) + + def handle_status(self, content): + serializer = StatSerializer(data=content) + if not serializer.is_valid(): + logger.error('Invalid status data: {}'.format(serializer.errors)) + return + serializer.validated_data["terminal"] = self.terminal + session_ids = serializer.validated_data.pop('sessions', []) + Session.set_sessions_active(session_ids) + with safe_db_connection(): + serializer.save() + + def send_kill_tasks_msg(self, task_id=None): + content = self.get_terminal_tasks(task_id) + self.send(bytes_data=content) + + def get_terminal_tasks(self, task_id=None): + with safe_db_connection(): + critical_time = timezone.now() - datetime.timedelta(minutes=10) + tasks = self.terminal.task_set.filter(is_finished=False, date_created__gte=critical_time) + if task_id: + tasks = tasks.filter(id=task_id) + serializer = TaskSerializer(tasks, many=True) + return JSONRenderer().render(serializer.data) + + def watch_component_event(self): + # 先发一次已有的任务 + self.send_kill_tasks_msg() + + ws = self + + def handle_task_msg_recv(msg): + logger.debug('New component task msg recv: {}'.format(msg)) + msg_type = msg.get('type') + payload = msg.get('payload') + if msg_type == "kill_session": + ws.send_kill_tasks_msg(payload.get('id')) + + return component_event_chan.subscribe(handle_task_msg_recv) + + def disconnect(self, code): + if self.sub is None: + return + self.sub.unsubscribe() diff --git a/apps/tickets/api/flow.py b/apps/tickets/api/flow.py index 303af6d3f..b11b1bb70 100644 --- a/apps/tickets/api/flow.py +++ b/apps/tickets/api/flow.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import MethodNotAllowed from tickets import serializers from tickets.models import TicketFlow -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet __all__ = ['TicketFlowViewSet'] diff --git a/apps/tickets/api/relation.py b/apps/tickets/api/relation.py index c442c04ab..2f12437cb 100644 --- a/apps/tickets/api/relation.py +++ b/apps/tickets/api/relation.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from rest_framework.mixins import CreateModelMixin from orgs.utils import tmp_to_root_org -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from terminal.serializers import SessionSerializer from tickets.models import TicketSession from tickets.serializers import TicketSessionRelationSerializer diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index a402e23a1..19bffa952 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import MethodNotAllowed from orgs.utils import tmp_to_root_org from rbac.permissions import RBACPermission -from common.mixins.api import CommonApiMixin +from common.api import CommonApiMixin from common.const.http import POST, PUT, PATCH from tickets import filters from tickets import serializers @@ -86,7 +86,7 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): instance.reject(processor=request.user) return Response('ok') - @action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ]) + @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) def close(self, request, *args, **kwargs): instance = self.get_object() instance.close() diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py index 40fb314e3..e71d4297a 100644 --- a/apps/tickets/handlers/base.py +++ b/apps/tickets/handlers/base.py @@ -98,7 +98,7 @@ class BaseHandler: approve_info = _('{} {} the ticket').format(user_display, state_display) context = self._diff_prev_approve_context(state) context.update({'approve_info': approve_info}) - body = self.reject_html_script( + body = self.safe_html_script( render_to_string('tickets/ticket_approve_diff.html', context) ) data = { @@ -111,6 +111,6 @@ class BaseHandler: return self.ticket.comments.create(**data) @staticmethod - def reject_html_script(unsafe_html): + def safe_html_script(unsafe_html): safe_html = escape(unsafe_html) return safe_html diff --git a/apps/tickets/handlers/login_asset_confirm.py b/apps/tickets/handlers/login_asset_confirm.py index 16f156d4d..fa989e8a0 100644 --- a/apps/tickets/handlers/login_asset_confirm.py +++ b/apps/tickets/handlers/login_asset_confirm.py @@ -4,3 +4,9 @@ from .base import BaseHandler class Handler(BaseHandler): ticket: ApplyLoginAssetTicket + + def _on_step_approved(self, step): + is_finished = super()._on_step_approved(step) + if is_finished: + self.ticket.activate_connection_token_if_need() + return is_finished diff --git a/apps/tickets/migrations/0028_remove_app_tickets.py b/apps/tickets/migrations/0028_remove_app_tickets.py new file mode 100644 index 000000000..b396d6c2a --- /dev/null +++ b/apps/tickets/migrations/0028_remove_app_tickets.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-12-28 09:46 + +from django.db import migrations + + +def migrate_remove_app_tickets(apps, *args): + model = apps.get_model('tickets', 'Ticket') + model.objects.filter(type='apply_application').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('tickets', '0027_alter_applycommandticket_apply_run_account'), + ] + + operations = [ + migrations.RunPython(migrate_remove_app_tickets) + ] diff --git a/apps/tickets/migrations/0029_auto_20230110_1445.py b/apps/tickets/migrations/0029_auto_20230110_1445.py new file mode 100644 index 000000000..a6757b534 --- /dev/null +++ b/apps/tickets/migrations/0029_auto_20230110_1445.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0110_alter_favoriteasset_options'), + ('tickets', '0028_remove_app_tickets'), + ] + + operations = [ + migrations.AlterModelOptions( + name='applyassetticket', + options={'verbose_name': 'Apply Asset Ticket'}, + ), + migrations.AlterField( + model_name='applyassetticket', + name='apply_assets', + field=models.ManyToManyField(to='assets.Asset', verbose_name='Asset'), + ), + migrations.AlterField( + model_name='applyassetticket', + name='apply_nodes', + field=models.ManyToManyField(to='assets.Node', verbose_name='Node'), + ), + ] diff --git a/apps/tickets/models/comment.py b/apps/tickets/models/comment.py index 638ecdc4d..6577b65e2 100644 --- a/apps/tickets/models/comment.py +++ b/apps/tickets/models/comment.py @@ -33,3 +33,6 @@ class Comment(JMSBaseModel): def set_display_fields(self): self.user_display = str(self.user) + + def __str__(self): + return str(self.ticket) diff --git a/apps/tickets/models/ticket/apply_asset.py b/apps/tickets/models/ticket/apply_asset.py index 2fde56125..ef29c607f 100644 --- a/apps/tickets/models/ticket/apply_asset.py +++ b/apps/tickets/models/ticket/apply_asset.py @@ -11,9 +11,9 @@ asset_or_node_help_text = _("Select at least one asset or node") class ApplyAssetTicket(Ticket): apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name')) - apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes')) + apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Node')) # 申请信息 - apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets')) + apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Asset')) apply_accounts = models.JSONField(default=list, verbose_name=_('Apply accounts')) apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all()) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) @@ -21,3 +21,6 @@ class ApplyAssetTicket(Ticket): def get_apply_actions_display(self): return ActionChoices.display(self.apply_actions) + + class Meta: + verbose_name = _('Apply Asset Ticket') diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py index 863a1508f..11a1db9dc 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -14,6 +14,7 @@ from common.db.encoder import ModelJSONFieldEncoder from common.db.models import JMSBaseModel from common.exceptions import JMSException from common.utils.timezone import as_current_tz +from common.utils import reverse from orgs.models import Organization from orgs.utils import tmp_to_org from tickets.const import ( @@ -250,9 +251,8 @@ class StatusMixin: @property def processor(self): - processor = self.current_step.ticket_assignees \ - .exclude(state=StepState.pending).first() - return processor.assignee if processor else None + """ 返回最后一步的处理人 """ + return self.current_step.processor def has_current_assignee(self, assignee): return self.ticket_steps.filter( @@ -418,6 +418,39 @@ class Ticket(StatusMixin, JMSBaseModel): snapshot[field.verbose_name] = value return snapshot + def get_extra_info_of_review(self, user=None): + if user and user.is_service_account: + url_ticket_status = reverse( + view_name='api-tickets:super-ticket-status', kwargs={'pk': str(self.id)} + ) + check_ticket_api = {'method': 'GET', 'url': url_ticket_status} + close_ticket_api = {'method': 'DELETE', 'url': url_ticket_status} + else: + url_ticket_status = reverse( + view_name='api-tickets:ticket-detail', kwargs={'pk': str(self.id)} + ) + url_ticket_close = reverse( + view_name='api-tickets:ticket-close', kwargs={'pk': str(self.id)} + ) + check_ticket_api = {'method': 'GET', 'url': url_ticket_status} + close_ticket_api = {'method': 'PUT', 'url': url_ticket_close} + + url_ticket_detail_external = reverse( + view_name='api-tickets:ticket-detail', + kwargs={'pk': str(self.id)}, + external=True, + api_to_ui=True + ) + ticket_assignees = self.current_step.ticket_assignees.all() + return { + 'check_ticket_api': check_ticket_api, + 'close_ticket_api': close_ticket_api, + 'ticket_detail_page_url': '{url}?type={type}'.format( + url=url_ticket_detail_external, type=self.type + ), + 'assignees': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees] + } + class SuperTicket(Ticket): class Meta: diff --git a/apps/tickets/models/ticket/login_asset_confirm.py b/apps/tickets/models/ticket/login_asset_confirm.py index 8761bc7fe..ffcbf869e 100644 --- a/apps/tickets/models/ticket/login_asset_confirm.py +++ b/apps/tickets/models/ticket/login_asset_confirm.py @@ -16,3 +16,9 @@ class ApplyLoginAssetTicket(Ticket): apply_login_account = models.CharField( max_length=128, default='', verbose_name=_('Login account') ) + + def activate_connection_token_if_need(self): + if not self.connection_token: + return + self.connection_token.is_active = True + self.connection_token.save() diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py index 6fa9fa567..09ce04232 100644 --- a/apps/tickets/serializers/comment.py +++ b/apps/tickets/serializers/comment.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from common.drf.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField from ..models import Comment __all__ = ['CommentSerializer'] @@ -24,7 +24,7 @@ class CommentSerializer(serializers.ModelSerializer): model = Comment fields_mini = ['id'] fields_small = fields_mini + [ - 'body', 'user_display', 'date_created', 'date_updated' + 'body', 'user_display', 'date_created', 'date_updated' ] fields_fk = ['ticket', 'user', ] fields = fields_small + fields_fk diff --git a/apps/tickets/serializers/flow.py b/apps/tickets/serializers/flow.py index f48e16501..f3b9c0215 100644 --- a/apps/tickets/serializers/flow.py +++ b/apps/tickets/serializers/flow.py @@ -2,11 +2,10 @@ from rest_framework import serializers from django.db.transaction import atomic from django.utils.translation import ugettext_lazy as _ - 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 common.serializers.fields import LabeledChoiceField from tickets.models import TicketFlow, ApprovalRule from tickets.const import TicketApprovalStrategy, TicketType diff --git a/apps/tickets/serializers/super_ticket.py b/apps/tickets/serializers/super_ticket.py index 9200c3f28..12bb911b0 100644 --- a/apps/tickets/serializers/super_ticket.py +++ b/apps/tickets/serializers/super_ticket.py @@ -1,5 +1,7 @@ from rest_framework import serializers from django.utils.translation import gettext_lazy as _ +from common.serializers.fields import LabeledChoiceField +from tickets.const import TicketStatus, TicketState from ..models import SuperTicket @@ -8,6 +10,8 @@ __all__ = ['SuperTicketSerializer'] class SuperTicketSerializer(serializers.ModelSerializer): + status = LabeledChoiceField(choices=TicketStatus.choices, read_only=True, label=_('Status')) + state = LabeledChoiceField(choices=TicketState.choices, read_only=True, label=_("State")) processor = serializers.SerializerMethodField(label=_("Processor")) class Meta: @@ -16,6 +20,4 @@ class SuperTicketSerializer(serializers.ModelSerializer): @staticmethod def get_processor(instance): - if not instance.processor: - return '' - return str(instance.processor) + return str(instance.processor) if instance.processor else '' diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index 8bf7b8563..97c42f331 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from assets.models import Asset, Node from perms.models import AssetPermission from perms.serializers.permission import ActionChoicesField -from common.drf.fields import ObjectRelatedField +from common.serializers.fields import ObjectRelatedField from tickets.models import ApplyAssetTicket from .common import BaseApplyAssetSerializer from .ticket import TicketApplySerializer diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index d21c44ec0..334031e5e 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.models import Organization from tickets.const import TicketType, TicketStatus, TicketState @@ -29,9 +29,7 @@ class TicketSerializer(OrgResourceModelSerializerMixin): 'status', 'date_created', 'date_updated', 'org_name', 'rel_snapshot' ] fields = fields_small + read_only_fields - extra_kwargs = { - 'type': {'required': True} - } + extra_kwargs = {} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -85,15 +83,10 @@ class TicketApplySerializer(TicketSerializer): ticket_type = attrs.get('type') org_id = attrs.get('org_id') - flow = TicketFlow.get_org_related_flows(org_id=org_id) \ - .filter(type=ticket_type).first() - - if flow: - attrs['flow'] = flow - return attrs - else: + flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first() + if not flow: error = _('The ticket flow `{}` does not exist'.format(ticket_type)) raise serializers.ValidationError(error) - + attrs['flow'] = flow attrs['applicant'] = self.get_applicant(attrs.get('applicant')) return attrs diff --git a/apps/users/api/relation.py b/apps/users/api/relation.py index 23260a5ae..5af72633c 100644 --- a/apps/users/api/relation.py +++ b/apps/users/api/relation.py @@ -3,7 +3,7 @@ from django.db.models import F -from common.drf.api import JMSBulkRelationModelViewSet +from common.api import JMSBulkRelationModelViewSet from .. import serializers from ..models import User, UserGroup diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 70939f51f..c882719f6 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -7,9 +7,9 @@ from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet -from common.mixins import CommonApiMixin +from common.api import CommonApiMixin from common.utils import get_logger -from common.mixins.api import SuggestionMixin +from common.api import SuggestionMixin from orgs.utils import current_org, tmp_to_root_org from rbac.models import Role, RoleBinding from users.utils import LoginBlockUtil, MFABlockUtils diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index c8dfb7782..287454146 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.utils import validate_ssh_public_key -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from ..models import User from .user import UserSerializer diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 1d99be5ac..7baa4117b 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -5,8 +5,8 @@ from functools import partial from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField -from common.drf.serializers import CommonBulkSerializerMixin +from common.serializers.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField +from common.serializers import CommonBulkSerializerMixin from common.utils import pretty_string, get_logger from common.validators import PhoneValidator from rbac.builtin import BuiltinRole diff --git a/apps/users/views/profile/mfa.py b/apps/users/views/profile/mfa.py index 432dba5c8..10f289ab8 100644 --- a/apps/users/views/profile/mfa.py +++ b/apps/users/views/profile/mfa.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.views.generic.base import TemplateView from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin from users.models import User __all__ = ['MFASettingView'] @@ -21,4 +21,3 @@ class MFASettingView(PermissionsMixin, TemplateView): 'mfa_backends': mfa_backends, }) return context - diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py index fec3055e6..5b4333d37 100644 --- a/apps/users/views/profile/otp.py +++ b/apps/users/views/profile/otp.py @@ -13,7 +13,7 @@ from authentication.mixins import AuthMixin from authentication.mfa import MFAOtp, otp_failed_msg from authentication.errors import SessionEmptyError from common.utils import get_logger, FlashMessageUtil -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin from common.permissions import IsValidUser from .password import UserVerifyPasswordView from ... import forms @@ -168,5 +168,3 @@ class UserOtpDisableView(PermissionsMixin, FormView): } url = FlashMessageUtil.gen_message_url(message_data) return url - - diff --git a/apps/users/views/profile/pubkey.py b/apps/users/views/profile/pubkey.py index 7408889e1..f8a038e50 100644 --- a/apps/users/views/profile/pubkey.py +++ b/apps/users/views/profile/pubkey.py @@ -5,7 +5,7 @@ from django.views import View from common.utils import get_logger, ssh_key_gen from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin __all__ = ['UserPublicKeyGenerateView'] diff --git a/generateV3Data.py b/generateV3Data.py deleted file mode 100644 index 673037e99..000000000 --- a/generateV3Data.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python -# - -# >>> Django 环境配置 -import django -import os -import sys - -if os.path.exists('../apps'): - sys.path.insert(0, '../apps') -elif os.path.exists('./apps'): - sys.path.insert(0, './apps') - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -APPS_DIR = os.path.join(BASE_DIR, 'apps') -sys.path.insert(0, APPS_DIR) - -os.environ.setdefault('PYTHONOPTIMIZE', '1') -if os.getuid() == 0: - os.environ.setdefault('C_FORCE_ROOT', '1') - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings") -django.setup() - -# <<< - - -class Generator(object): - - def generate(self): - pass - - def generate_assets(self): - pass - - -if __name__ == '__main__': - pass