diff --git a/apps/assets/automations/change_secret/manager.py b/apps/assets/automations/change_secret/manager.py index 4f77dfe85..a8b7dd515 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/assets/automations/change_secret/manager.py @@ -1,17 +1,28 @@ +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 common.utils import lazyproperty, gen_key_pair +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 +logger = get_logger(__name__) + class ChangeSecretManager(BasePlaybookManager): def __init__(self, *args, **kwargs): @@ -125,7 +136,7 @@ class ChangeSecretManager(BasePlaybookManager): new_secret = self.get_secret() recorder = ChangeSecretRecord( - account=account, execution=self.execution, + asset=asset, account=account, execution=self.execution, old_secret=account.secret, new_secret=new_secret, ) records.append(recorder) @@ -172,4 +183,49 @@ class ChangeSecretManager(BasePlaybookManager): recorder.save() def on_runner_failed(self, runner, e): - pass + logger.error("Change secret error: ", e) + + def run(self, *args, **kwargs): + super().run(*args, **kwargs) + recorders = self.name_recorder_mapper.values() + recorders = list(recorders) + self.send_recorder_mail(recorders) + + def send_recorder_mail(self, recorders): + recipients = self.execution.recipients + if not recorders or not recipients: + return + recipients = User.objects.filter(id__in=list(recipients)) + + name = self.execution.snapshot['name'] + path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') + filename = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.xlsx') + if not self.create_file(recorders, filename): + return + + for user in recipients: + attachments = [] + if user.secret_key: + password = user.secret_key.encode('utf8') + attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip') + encrypt_and_compress_zip_file(attachment, password, [filename]) + attachments = [attachment] + ChangeSecretExecutionTaskMsg(name, user).publish(attachments) + os.remove(filename) + + @staticmethod + def create_file(recorders, filename): + serializer_cls = ChangeSecretRecordBackUpSerializer + serializer = serializer_cls(recorders, many=True) + header = [v.label for v in serializer.child.fields.values()] + rows = [list(row.values()) for row in serializer.data] + if not rows: + return False + + rows.insert(0, header) + wb = Workbook(filename) + ws = wb.create_sheet('Sheet1') + for row in rows: + ws.append(row) + wb.save(filename) + return True diff --git a/apps/assets/migrations/0109_auto_20221102_2017.py b/apps/assets/migrations/0109_auto_20221102_2017.py new file mode 100644 index 000000000..a9dcc4446 --- /dev/null +++ b/apps/assets/migrations/0109_auto_20221102_2017.py @@ -0,0 +1,53 @@ +# 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/0109_rename_categories_to_types.py b/apps/assets/migrations/0109_rename_categories_to_types.py deleted file mode 100644 index aa7fcad8f..000000000 --- a/apps/assets/migrations/0109_rename_categories_to_types.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-03 08:44 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0108_auto_20221027_1053'), - ] - - operations = [ - migrations.RenameField( - model_name='accountbackupplan', - old_name='categories', - new_name='types', - ), - ] diff --git a/apps/assets/migrations/0110_changesecretrecord_asset.py b/apps/assets/migrations/0110_changesecretrecord_asset.py new file mode 100644 index 000000000..76a3f9872 --- /dev/null +++ b/apps/assets/migrations/0110_changesecretrecord_asset.py @@ -0,0 +1,24 @@ +# 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/models/account.py b/apps/assets/models/account.py index c2bb40a99..9aa007e53 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -30,11 +30,13 @@ class AccountHistoricalRecords(HistoricalRecords): return super().post_save(instance, created, using=using, **kwargs) - def fields_included(self, model): - if self.included_fields: - fields = [i for i in model._meta.fields if i.name in self.included_fields] - return fields - return super().fields_included(model) + def create_history_model(self, model, inherited): + if self.included_fields and not self.excluded_fields: + self.excluded_fields = [ + field.name for field in model._meta.fields + if field.name not in self.included_fields + ] + return super().create_history_model(model, inherited) class Account(AbsConnectivity, BaseAccount): diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index ac1fdb046..5eadca8c4 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -111,6 +111,13 @@ class AutomationExecution(OrgModelMixin): def manager_type(self): return self.snapshot['type'] + @property + def recipients(self): + recipients = self.snapshot.get('recipients') + if not recipients: + return [] + return recipients.values() + def start(self): from assets.automations.endpoint import ExecutionManager manager = ExecutionManager(execution=self) diff --git a/apps/assets/models/automations/change_secret.py b/apps/assets/models/automations/change_secret.py index 53ca08aba..81871fb3b 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/assets/models/automations/change_secret.py @@ -51,6 +51,7 @@ class ChangeSecretAutomation(BaseAutomation): class ChangeSecretRecord(JMSBaseModel): execution = models.ForeignKey('assets.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) old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret')) new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 9f4f05649..1194b7957 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -2,7 +2,6 @@ # import io import os -import uuid import sshpubkeys from hashlib import md5 @@ -12,13 +11,13 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.db.models import QuerySet -from common.db import fields from common.utils import ( ssh_key_string_to_obj, ssh_key_gen, get_logger, random_string, ssh_pubkey_gen, lazyproperty ) +from common.db import fields +from orgs.mixins.models import JMSOrgBaseModel from assets.const import Connectivity, SecretType -from orgs.mixins.models import OrgModelMixin logger = get_logger(__file__) @@ -47,8 +46,7 @@ class AbsConnectivity(models.Model): abstract = True -class BaseAccount(OrgModelMixin): - id = models.UUIDField(default=uuid.uuid4, primary_key=True) +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( @@ -56,9 +54,8 @@ class BaseAccount(OrgModelMixin): ) 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")) comment = models.TextField(blank=True, 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')) @property @@ -116,7 +113,7 @@ class BaseAccount(OrgModelMixin): @property def private_key_path(self): - if not self.secret_type != 'ssh_key' or not self.secret: + if not self.secret_type != SecretType.ssh_key or not self.secret: return None project_dir = settings.PROJECT_DIR tmp_dir = os.path.join(project_dir, 'tmp') @@ -158,7 +155,6 @@ class BaseAccount(OrgModelMixin): return { 'name': self.name, 'username': self.username, - 'password': self.password, 'public_key': self.public_key, } diff --git a/apps/assets/notifications.py b/apps/assets/notifications.py index 58c02686c..a797bc845 100644 --- a/apps/assets/notifications.py +++ b/apps/assets/notifications.py @@ -15,11 +15,35 @@ 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. 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) + "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.delay( + send_mail_attachment_async( self.subject, self.message, [self.user.email], attachment_list ) + + +class ChangeSecretExecutionTaskMsg(object): + subject = _('Notification of implementation result of encryption change plan') + + def __init__(self, name: str, user: User): + self.name = name + self.user = user + + @property + 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) + + def publish(self, attachments=None): + send_mail_attachment_async( + self.subject, self.message, [self.user.email], attachments + ) diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 586c6f2e5..252b2dc64 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -11,3 +11,4 @@ from .account import * from assets.serializers.account.backup import * from .platform import * from .cagegory import * +from .automation import * diff --git a/apps/assets/serializers/account/account.py b/apps/assets/serializers/account/account.py index b219211c2..efd8d9060 100644 --- a/apps/assets/serializers/account/account.py +++ b/apps/assets/serializers/account/account.py @@ -3,6 +3,7 @@ from rest_framework import serializers from common.drf.serializers import SecretReadableMixin from common.drf.fields import ObjectRelatedField +from assets.tasks import push_accounts_to_assets from assets.models import Account, AccountTemplate, Asset from .base import BaseAccountSerializer @@ -47,8 +48,7 @@ class AccountSerializerCreateMixin(serializers.ModelSerializer): def create(self, validated_data): instance = super().create(validated_data) if self.push_now: - # Todo: push it - print("Start push account to asset") + push_accounts_to_assets.delay([instance.id], [instance.asset_id]) return instance diff --git a/apps/assets/serializers/account/template.py b/apps/assets/serializers/account/template.py index 09c5b4541..4599ca0e7 100644 --- a/apps/assets/serializers/account/template.py +++ b/apps/assets/serializers/account/template.py @@ -1,6 +1,3 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - from assets.models import AccountTemplate from .base import BaseAccountSerializer @@ -9,15 +6,14 @@ class AccountTemplateSerializer(BaseAccountSerializer): class Meta(BaseAccountSerializer.Meta): model = AccountTemplate - @classmethod - def validate_required(cls, attrs): - # Todo: why ? - required_field_dict = {} - error = _('This field is required.') - for k, v in cls().fields.items(): - if v.required and k not in attrs: - required_field_dict[k] = error - if not required_field_dict: - return - raise serializers.ValidationError(required_field_dict) - + # @classmethod + # def validate_required(cls, attrs): + # # TODO 选择模版后检查一些必填项 + # required_field_dict = {} + # error = _('This field is required.') + # for k, v in cls().fields.items(): + # if v.required and k not in attrs: + # required_field_dict[k] = error + # if not required_field_dict: + # return + # raise serializers.ValidationError(required_field_dict) diff --git a/apps/assets/serializers/automation.py b/apps/assets/serializers/automation.py new file mode 100644 index 000000000..482f95fc8 --- /dev/null +++ b/apps/assets/serializers/automation.py @@ -0,0 +1,35 @@ +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from common.utils import get_logger + +from assets.models import ChangeSecretRecord + +logger = get_logger(__file__) + + +class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer): + asset = serializers.SerializerMethodField(label=_('Asset')) + account = serializers.SerializerMethodField(label=_('Account')) + is_success = serializers.SerializerMethodField(label=_('Is success')) + + class Meta: + model = ChangeSecretRecord + fields = [ + 'id', 'asset', 'account', 'old_secret', 'new_secret', + 'status', 'error', 'is_success' + ] + + @staticmethod + def get_asset(instance): + return str(instance.asset) + + @staticmethod + def get_account(instance): + return str(instance.account) + + @staticmethod + def get_is_success(obj): + if obj.status == 'success': + return _("Success") + return _("Failed") diff --git a/apps/assets/signal_handlers/account.py b/apps/assets/signal_handlers/account.py index 26cf75d02..8020e4087 100644 --- a/apps/assets/signal_handlers/account.py +++ b/apps/assets/signal_handlers/account.py @@ -9,8 +9,6 @@ logger = get_logger(__name__) @receiver(pre_save, sender=Account) def on_account_pre_create(sender, instance, **kwargs): - # Todo: 是否只有更改密码的时候才有版本增加, bitwarden 只有再改密码的时候, - # 才会有版本,代表的是 password_version # 升级版本号 instance.version += 1 # 即使在 root 组织也不怕 diff --git a/apps/assets/tasks/push_account.py b/apps/assets/tasks/push_account.py index 19ebd5045..8af71cd3f 100644 --- a/apps/assets/tasks/push_account.py +++ b/apps/assets/tasks/push_account.py @@ -11,8 +11,9 @@ __all__ = [ @org_aware_func("assets") -def push_accounts_to_assets_util(accounts, assets, task_name): +def push_accounts_to_assets_util(accounts, assets): from assets.models import PushAccountAutomation + task_name = gettext_noop("Push accounts to assets") task_name = PushAccountAutomation.generate_unique_name(task_name) account_usernames = list(accounts.values_list('username', flat=True)) @@ -33,5 +34,4 @@ def push_accounts_to_assets(account_ids, asset_ids): assets = Asset.objects.get(id=asset_ids) accounts = Account.objects.get(id=account_ids) - task_name = gettext_noop("Push accounts to assets") - return push_accounts_to_assets_util(accounts, assets, task_name) + return push_accounts_to_assets_util(accounts, assets) diff --git a/apps/common/drf/serializers/common.py b/apps/common/drf/serializers/common.py index a522e3a82..805aaa453 100644 --- a/apps/common/drf/serializers/common.py +++ b/apps/common/drf/serializers/common.py @@ -13,8 +13,8 @@ from .mixin import BulkListSerializerMixin, BulkSerializerMixin __all__ = [ 'MethodSerializer', 'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskExecutionSerializer', - 'WritableNestedModelSerializer', - 'GroupedChoiceSerializer', + 'WritableNestedModelSerializer', 'GroupedChoiceSerializer', + 'FileSerializer' ] @@ -88,3 +88,7 @@ class GroupedChoiceSerializer(ChoiceSerializer): class WritableNestedModelSerializer(NestedModelSerializer): pass + + +class FileSerializer(serializers.Serializer): + file = serializers.FileField(label=_("File")) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index c95e1d78e..093842b71 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:c4c889e251a4de3161f462e042882ba3c4ab40eaf34799e2d49d4788ad961586 -size 119171 +oid sha256:07f1cfd07039142f4847b4139586bf815467f266119eae57476c073130f0ac92 +size 118098 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 4b15fdcde..59f5db0eb 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-11-01 15:30+0800\n" +"POT-Creation-Date: 2022-11-03 16:00+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -25,7 +25,7 @@ msgstr "Acls" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:48 #: applications/models.py:10 assets/models/_user.py:33 #: assets/models/asset/common.py:81 assets/models/asset/common.py:91 -#: assets/models/base.py:59 assets/models/cmd_filter.py:25 +#: assets/models/base.py:57 assets/models/cmd_filter.py:25 #: assets/models/domain.py:24 assets/models/group.py:20 #: assets/models/label.py:17 assets/models/platform.py:22 #: assets/models/platform.py:68 assets/serializers/asset/common.py:86 @@ -60,7 +60,7 @@ msgstr "アクティブ" #: acls/models/base.py:32 applications/models.py:19 assets/models/_user.py:40 #: assets/models/asset/common.py:100 assets/models/automations/base.py:26 -#: assets/models/backup.py:30 assets/models/base.py:66 +#: assets/models/backup.py:30 assets/models/base.py:65 #: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88 #: assets/models/domain.py:25 assets/models/domain.py:69 #: assets/models/group.py:23 assets/models/label.py:22 @@ -68,7 +68,7 @@ msgstr "アクティブ" #: ops/models/playbook.py:25 orgs/models.py:74 #: perms/models/asset_permission.py:84 rbac/models/role.py:37 #: settings/models.py:38 terminal/models/applet/applet.py:28 -#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:34 +#: terminal/models/applet/applet.py:61 terminal/models/applet/host.py:104 #: terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:97 #: terminal/models/component/storage.py:28 @@ -129,14 +129,14 @@ msgstr "レビュー担当者" msgid "Login acl" msgstr "ログインacl" -#: acls/models/login_asset_acl.py:21 assets/models/account.py:57 +#: acls/models/login_asset_acl.py:21 assets/models/account.py:59 #: authentication/models/connection_token.py:33 ops/models/base.py:18 #: terminal/models/session/session.py:34 xpack/plugins/cloud/models.py:87 #: xpack/plugins/cloud/serializers/task.py:65 msgid "Account" msgstr "アカウント" -#: acls/models/login_asset_acl.py:22 assets/models/account.py:47 +#: acls/models/login_asset_acl.py:22 assets/models/account.py:49 #: assets/models/asset/common.py:83 assets/models/asset/common.py:227 #: assets/models/cmd_filter.py:36 assets/models/gathered_user.py:14 #: assets/serializers/account/account.py:58 assets/serializers/label.py:30 @@ -164,7 +164,7 @@ msgstr "コンマ区切り文字列の形式。* はすべて一致すること #: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:18 #: acls/serializers/login_asset_acl.py:52 assets/models/_user.py:34 -#: assets/models/base.py:60 assets/models/gathered_user.py:15 +#: assets/models/base.py:58 assets/models/gathered_user.py:15 #: audits/models.py:121 authentication/forms.py:25 authentication/forms.py:27 #: authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 @@ -210,7 +210,7 @@ msgstr "" "ション: {}" #: acls/serializers/login_asset_acl.py:84 -#: tickets/serializers/ticket/ticket.py:85 +#: tickets/serializers/ticket/ticket.py:86 msgid "The organization `{}` does not exist" msgstr "組織 '{}'は存在しません" @@ -255,7 +255,7 @@ msgstr "カテゴリ" #: assets/models/platform.py:70 assets/serializers/asset/common.py:63 #: assets/serializers/platform.py:75 terminal/models/applet/applet.py:24 #: terminal/models/component/storage.py:57 -#: terminal/models/component/storage.py:142 terminal/serializers/applet.py:20 +#: terminal/models/component/storage.py:142 terminal/serializers/applet.py:33 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:17 #: tickets/models/ticket/general.py:273 @@ -303,7 +303,7 @@ msgstr "アプリ資産" msgid "{} disabled" msgstr "無効" -#: assets/const/account.py:6 assets/tasks/const.py:51 audits/const.py:5 +#: assets/const/account.py:6 audits/const.py:5 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" @@ -313,14 +313,14 @@ msgstr "不明" msgid "Ok" msgstr "OK" -#: assets/const/account.py:8 audits/models.py:118 common/const/choices.py:18 +#: assets/const/account.py:8 audits/models.py:118 common/const/choices.py:19 #: xpack/plugins/change_auth_plan/serializers/asset.py:190 #: xpack/plugins/cloud/const.py:33 msgid "Failed" msgstr "失敗しました" #: assets/const/account.py:12 assets/models/_user.py:35 -#: assets/models/base.py:53 assets/models/domain.py:71 +#: assets/models/base.py:52 assets/models/domain.py:71 #: assets/serializers/base.py:15 audits/signal_handlers.py:50 #: authentication/confirm/password.py:9 authentication/forms.py:32 #: authentication/templates/authentication/login.html:228 @@ -337,19 +337,19 @@ msgstr "失敗しました" msgid "Password" msgstr "パスワード" -#: assets/const/account.py:13 assets/models/base.py:54 +#: assets/const/account.py:13 assets/models/base.py:53 #, fuzzy #| msgid "SSH Key" msgid "SSH key" msgstr "SSHキー" -#: assets/const/account.py:14 assets/models/base.py:55 +#: assets/const/account.py:14 assets/models/base.py:54 #: authentication/models/access_key.py:31 msgid "Access key" msgstr "アクセスキー" #: assets/const/account.py:15 assets/models/_user.py:38 -#: assets/models/base.py:56 authentication/models/sso_token.py:13 +#: assets/models/base.py:55 authentication/models/sso_token.py:13 msgid "Token" msgstr "トークン" @@ -417,7 +417,7 @@ msgid "Replace (The key generated by JumpServer) " msgstr "置換(JumpServerによって生成された鍵)" #: assets/const/category.py:11 settings/serializers/auth/radius.py:14 -#: settings/serializers/auth/sms.py:56 terminal/models/applet/applet.py:56 +#: settings/serializers/auth/sms.py:56 terminal/models/applet/applet.py:59 #: terminal/models/component/endpoint.py:12 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" @@ -499,22 +499,20 @@ msgstr "SSH秘密鍵" msgid "SSH public key" msgstr "SSHパブリックキー" -#: assets/models/_user.py:41 assets/models/automations/base.py:87 -#: assets/models/base.py:67 assets/models/domain.py:26 -#: assets/models/gathered_user.py:19 assets/models/group.py:22 -#: common/db/models.py:76 common/mixins/models.py:50 ops/models/base.py:53 -#: orgs/models.py:73 perms/models/asset_permission.py:82 +#: assets/models/_user.py:41 assets/models/automations/base.py:96 +#: assets/models/domain.py:26 assets/models/gathered_user.py:19 +#: assets/models/group.py:22 common/db/models.py:76 common/mixins/models.py:50 +#: ops/models/base.py:53 orgs/models.py:73 perms/models/asset_permission.py:82 #: users/models/group.py:18 users/models/user.py:927 msgid "Date created" msgstr "作成された日付" -#: assets/models/_user.py:42 assets/models/base.py:68 -#: assets/models/gathered_user.py:20 common/db/models.py:77 -#: common/mixins/models.py:51 +#: assets/models/_user.py:42 assets/models/gathered_user.py:20 +#: common/db/models.py:77 common/mixins/models.py:51 msgid "Date updated" msgstr "更新日" -#: assets/models/_user.py:43 assets/models/base.py:69 +#: assets/models/_user.py:43 assets/models/base.py:66 #: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:91 #: assets/models/group.py:21 common/db/models.py:74 common/mixins/models.py:49 #: orgs/models.py:71 perms/models/asset_permission.py:81 @@ -582,34 +580,34 @@ msgstr "システムユーザー" msgid "Can match system user" msgstr "システムユーザーに一致できます" -#: assets/models/account.py:51 +#: assets/models/account.py:53 #, fuzzy #| msgid "Switch from" msgid "Su from" msgstr "から切り替え" -#: assets/models/account.py:53 settings/serializers/auth/cas.py:18 +#: assets/models/account.py:55 settings/serializers/auth/cas.py:18 #: terminal/models/applet/applet.py:22 msgid "Version" msgstr "バージョン" -#: assets/models/account.py:63 +#: assets/models/account.py:65 msgid "Can view asset account secret" msgstr "資産アカウントの秘密を表示できます" -#: assets/models/account.py:64 +#: assets/models/account.py:66 msgid "Can change asset account secret" msgstr "資産口座の秘密を変更できます" -#: assets/models/account.py:65 +#: assets/models/account.py:67 msgid "Can view asset history account" msgstr "資産履歴アカウントを表示できます" -#: assets/models/account.py:66 +#: assets/models/account.py:68 msgid "Can view asset history account secret" msgstr "資産履歴アカウントパスワードを表示できます" -#: assets/models/account.py:89 assets/serializers/account/account.py:13 +#: assets/models/account.py:91 assets/serializers/account/account.py:13 #, fuzzy #| msgid "Account name" msgid "Account template" @@ -642,9 +640,9 @@ msgid "Nodes" msgstr "ノード" #: assets/models/asset/common.py:98 assets/models/automations/base.py:25 -#: assets/models/cmd_filter.py:39 assets/models/domain.py:70 -#: assets/models/label.py:21 terminal/models/applet/applet.py:25 -#: users/serializers/user.py:147 +#: assets/models/base.py:64 assets/models/cmd_filter.py:39 +#: assets/models/domain.py:70 assets/models/label.py:21 +#: terminal/models/applet/applet.py:25 users/serializers/user.py:147 msgid "Is active" msgstr "アクティブです。" @@ -661,7 +659,9 @@ msgid "Can test asset connectivity" msgstr "資産接続をテストできます" #: assets/models/asset/common.py:232 -msgid "Can push system user to asset" +#, fuzzy +#| msgid "Can push system user to asset" +msgid "Can push account to asset" msgstr "システムユーザーを資産にプッシュできます" #: assets/models/asset/common.py:233 @@ -677,6 +677,7 @@ msgid "Move asset to node" msgstr "アセットをノードに移動する" #: assets/models/asset/web.py:9 audits/models.py:111 +#: terminal/serializers/applet_host.py:24 msgid "Disabled" msgstr "無効" @@ -723,15 +724,15 @@ msgstr "アカウント" msgid "Assets" msgstr "資産" -#: assets/models/automations/base.py:77 assets/models/automations/base.py:84 +#: assets/models/automations/base.py:86 assets/models/automations/base.py:93 #, fuzzy #| msgid "Automatic managed" msgid "Automation task" msgstr "自動管理" -#: assets/models/automations/base.py:88 assets/models/backup.py:77 +#: assets/models/automations/base.py:97 assets/models/backup.py:77 #: audits/models.py:44 ops/models/base.py:54 -#: perms/models/asset_permission.py:76 terminal/models/applet/host.py:32 +#: perms/models/asset_permission.py:76 terminal/models/applet/host.py:102 #: terminal/models/session/session.py:43 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:21 @@ -741,32 +742,32 @@ msgstr "自動管理" msgid "Date start" msgstr "開始日" -#: assets/models/automations/base.py:89 +#: assets/models/automations/base.py:98 #: assets/models/automations/change_secret.py:58 ops/models/base.py:55 -#: terminal/models/applet/host.py:33 +#: terminal/models/applet/host.py:103 msgid "Date finished" msgstr "終了日" -#: assets/models/automations/base.py:91 +#: assets/models/automations/base.py:100 #, fuzzy #| msgid "Relation snapshot" msgid "Automation snapshot" msgstr "製造オーダスナップショット" -#: assets/models/automations/base.py:95 assets/models/backup.py:88 +#: assets/models/automations/base.py:104 assets/models/backup.py:88 #: assets/serializers/account/backup.py:36 #: xpack/plugins/change_auth_plan/models/base.py:121 #: xpack/plugins/change_auth_plan/serializers/base.py:78 msgid "Trigger mode" msgstr "トリガーモード" -#: assets/models/automations/base.py:99 +#: assets/models/automations/base.py:108 #, fuzzy #| msgid "Command execution" msgid "Automation task execution" msgstr "コマンド実行" -#: assets/models/automations/change_secret.py:15 assets/models/base.py:62 +#: assets/models/automations/change_secret.py:15 assets/models/base.py:60 #, fuzzy #| msgid "Secret key" msgid "Secret type" @@ -779,7 +780,7 @@ msgid "Secret strategy" msgstr "SSHキー戦略" #: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:56 assets/models/base.py:64 +#: assets/models/automations/change_secret.py:56 assets/models/base.py:62 #: assets/serializers/account/base.py:17 #: authentication/models/connection_token.py:34 #: authentication/models/temp_token.py:10 @@ -825,7 +826,7 @@ msgstr "ひみつ" msgid "Date started" msgstr "開始日" -#: assets/models/automations/change_secret.py:60 common/const/choices.py:19 +#: assets/models/automations/change_secret.py:60 common/const/choices.py:20 #, fuzzy #| msgid "WeCom Error" msgid "Error" @@ -855,6 +856,12 @@ msgstr "資産ユーザーの収集" msgid "Gather asset facts" msgstr "資産ユーザーの収集" +#: assets/models/automations/ping.py:15 +#, fuzzy +#| msgid "Login asset" +msgid "Ping asset" +msgstr "ログイン資産" + #: assets/models/automations/push_account.py:16 #, fuzzy #| msgid "Is service account" @@ -902,15 +909,15 @@ msgstr "成功は" msgid "Account backup execution" msgstr "アカウントバックアップの実行" -#: assets/models/base.py:30 assets/serializers/domain.py:42 +#: assets/models/base.py:29 assets/serializers/domain.py:42 msgid "Connectivity" msgstr "接続性" -#: assets/models/base.py:32 authentication/models/temp_token.py:12 +#: assets/models/base.py:31 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "確認済みの日付" -#: assets/models/base.py:65 +#: assets/models/base.py:63 msgid "Privileged" msgstr "" @@ -1079,6 +1086,7 @@ msgid "Setting" msgstr "設定" #: assets/models/platform.py:43 audits/models.py:112 settings/models.py:37 +#: terminal/serializers/applet_host.py:25 msgid "Enabled" msgstr "有効化" @@ -1415,60 +1423,18 @@ msgstr "パスワードには `'` を含まない" msgid "Password can not contains `\"` " msgstr "パスワードには `\"` を含まない" -#: assets/tasks/account_connectivity.py:30 -msgid "The asset {} system platform {} does not support run Ansible tasks" -msgstr "" -"資産 {} システムプラットフォーム {} はAnsibleタスクの実行をサポートしていませ" -"ん。" - -#: assets/tasks/account_connectivity.py:108 -msgid "Test account connectivity: " -msgstr "テストアカウント接続:" - -#: assets/tasks/asset_connectivity.py:49 -msgid "Test assets connectivity. " -msgstr "資産の接続性をテストします。" - -#: assets/tasks/asset_connectivity.py:94 assets/tasks/asset_connectivity.py:107 -msgid "Test assets connectivity: " -msgstr "資産の接続性のテスト:" - -#: assets/tasks/asset_connectivity.py:121 -msgid "Test if the assets under the node are connectable: " -msgstr "ノードの下のアセットが接続可能かどうかをテストします。" - -#: assets/tasks/const.py:49 -msgid "Unreachable" -msgstr "達成できない" - -#: assets/tasks/const.py:50 -msgid "Reachable" -msgstr "接続可能" - -#: assets/tasks/gather_asset_hardware_info.py:46 -msgid "Get asset info failed: {}" -msgstr "資産情報の取得に失敗しました: {}" - -#: assets/tasks/gather_asset_hardware_info.py:97 +#: assets/tasks/gather_facts.py:25 msgid "Update some assets hardware info. " msgstr "一部の資産ハードウェア情報を更新します。" -#: assets/tasks/gather_asset_hardware_info.py:118 -msgid "Update asset hardware info: " -msgstr "資産ハードウェア情報の更新:" - -#: assets/tasks/gather_asset_hardware_info.py:124 +#: assets/tasks/gather_facts.py:48 msgid "Update assets hardware info: " msgstr "資産のハードウェア情報を更新する:" -#: assets/tasks/gather_asset_hardware_info.py:146 +#: assets/tasks/gather_facts.py:58 msgid "Update node asset hardware information: " msgstr "ノード資産のハードウェア情報を更新します。" -#: assets/tasks/gather_asset_users.py:110 -msgid "Gather assets users" -msgstr "資産ユーザーの収集" - #: assets/tasks/nodes_amount.py:29 msgid "" "The task of self-checking is already running and cannot be started repeatedly" @@ -1476,6 +1442,24 @@ msgstr "" "セルフチェックのタスクはすでに実行されており、繰り返し開始することはできませ" "ん" +#: assets/tasks/ping.py:20 assets/tasks/ping.py:38 +#, fuzzy +#| msgid "Test assets connectivity. " +msgid "Test assets connectivity " +msgstr "資産の接続性をテストします。" + +#: assets/tasks/ping.py:48 +#, fuzzy +#| msgid "Test if the assets under the node are connectable: " +msgid "Test if the assets under the node are connectable " +msgstr "ノードの下のアセットが接続可能かどうかをテストします。" + +#: assets/tasks/push_account.py:36 +#, fuzzy +#| msgid "Create account successfully" +msgid "Push accounts to assets" +msgstr "アカウントを正常に作成" + #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "資産が無効化されました。スキップ: {}" @@ -1492,6 +1476,12 @@ msgstr "セキュリティのために、ユーザー {} をプッシュしな msgid "No assets matched, stop task" msgstr "一致する資産がない、タスクを停止" +#: assets/tasks/verify_account.py:36 +#, fuzzy +#| msgid "Test account connectivity: " +msgid "Verify accounts connectivity" +msgstr "テストアカウント接続:" + #: audits/apps.py:9 msgid "Audits" msgstr "監査" @@ -1539,7 +1529,7 @@ msgstr "操作" msgid "Filename" msgstr "ファイル名" -#: audits/models.py:43 audits/models.py:117 common/const/choices.py:17 +#: audits/models.py:43 audits/models.py:117 common/const/choices.py:18 #: terminal/models/session/sharing.py:104 tickets/views/approve.py:114 #: xpack/plugins/change_auth_plan/serializers/asset.py:189 msgid "Success" @@ -1619,8 +1609,8 @@ msgid "MFA" msgstr "MFA" #: audits/models.py:128 ops/models/base.py:48 -#: terminal/models/applet/applet.py:57 terminal/models/applet/host.py:19 -#: terminal/models/applet/host.py:31 terminal/models/component/status.py:33 +#: terminal/models/applet/applet.py:60 terminal/models/applet/host.py:101 +#: terminal/models/component/status.py:33 terminal/serializers/applet.py:22 #: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:171 #: xpack/plugins/cloud/models.py:223 msgid "Status" @@ -2144,13 +2134,13 @@ msgstr "アセットがアクティブ化されていません" msgid "No account" msgstr "ログインacl" -#: authentication/models/connection_token.py:103 +#: authentication/models/connection_token.py:101 msgid "User has no permission to access asset or permission expired" msgstr "" "ユーザーがアセットにアクセスする権限を持っていないか、権限の有効期限が切れて" "います" -#: authentication/models/connection_token.py:145 +#: authentication/models/connection_token.py:144 msgid "Super connection token" msgstr "スーパー接続トークン" @@ -2576,15 +2566,19 @@ msgstr "手動トリガー" msgid "Timing trigger" msgstr "タイミングトリガー" -#: common/const/choices.py:15 tickets/const.py:29 tickets/const.py:37 +#: common/const/choices.py:15 xpack/plugins/change_auth_plan/models/base.py:183 +msgid "Ready" +msgstr "の準備を" + +#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:37 msgid "Pending" msgstr "未定" -#: common/const/choices.py:16 +#: common/const/choices.py:17 msgid "Running" msgstr "" -#: common/const/choices.py:20 +#: common/const/choices.py:21 #, fuzzy #| msgid "Cancel" msgid "Canceled" @@ -2653,6 +2647,12 @@ msgstr "解析ファイルエラー: {}" msgid "Children" msgstr "" +#: common/drf/serializers/common.py:94 +#, fuzzy +#| msgid "Filename" +msgid "File" +msgstr "ファイル名" + #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -3091,7 +3091,7 @@ msgstr "アプリ組織" #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:88 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 -#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71 +#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:72 msgid "Organization" msgstr "組織" @@ -4928,50 +4928,52 @@ msgstr "認証アドレス" msgid "Tags" msgstr "" -#: terminal/models/applet/applet.py:29 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:31 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "ホスト" -#: terminal/models/applet/applet.py:55 terminal/models/applet/host.py:21 +#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:28 #, fuzzy #| msgid "Apply assets" msgid "Applet" msgstr "資産の適用" -#: terminal/models/applet/host.py:14 -#, fuzzy -#| msgid "Verify auth" -msgid "Account automation" -msgstr "パスワード/キーの確認" - -#: terminal/models/applet/host.py:15 terminal/serializers/applet.py:66 +#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:36 #, fuzzy #| msgid "More login options" msgid "Deploy options" msgstr "その他のログインオプション" -#: terminal/models/applet/host.py:16 +#: terminal/models/applet/host.py:20 msgid "Inited" msgstr "" -#: terminal/models/applet/host.py:17 +#: terminal/models/applet/host.py:21 #, fuzzy #| msgid "Date finished" msgid "Date inited" msgstr "終了日" -#: terminal/models/applet/host.py:18 +#: terminal/models/applet/host.py:22 #, fuzzy #| msgid "Date sync" msgid "Date synced" msgstr "日付の同期" -#: terminal/models/applet/host.py:30 +#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:183 +msgid "Terminal" +msgstr "ターミナル" + +#: terminal/models/applet/host.py:99 #, fuzzy #| msgid "Host" msgid "Hosting" msgstr "ホスト" +#: terminal/models/applet/host.py:100 +msgid "Initial" +msgstr "" + #: terminal/models/component/endpoint.py:14 msgid "HTTPS Port" msgstr "HTTPS ポート" @@ -5076,10 +5078,6 @@ msgstr "再生ストレージ" msgid "type" msgstr "タイプ" -#: terminal/models/component/terminal.py:183 -msgid "Terminal" -msgstr "ターミナル" - #: terminal/models/component/terminal.py:185 msgid "Can view terminal config" msgstr "ターミナル構成を表示できます" @@ -5196,49 +5194,61 @@ msgstr "レベル" msgid "Batch danger command alert" msgstr "一括危険コマンド警告" -#: terminal/serializers/applet.py:19 +#: terminal/serializers/applet.py:16 +#, fuzzy +#| msgid "Public key" +msgid "Published" +msgstr "公開キー" + +#: terminal/serializers/applet.py:17 +#, fuzzy +#| msgid "Finished" +msgid "Unpublished" +msgstr "終了" + +#: terminal/serializers/applet.py:18 +#, fuzzy +#| msgid "Phone not set" +msgid "Not match" +msgstr "電話が設定されていない" + +#: terminal/serializers/applet.py:32 msgid "Icon" msgstr "" -#: terminal/serializers/applet.py:53 -#, fuzzy -#| msgid "Phone not set" -msgid "Not set" -msgstr "電話が設定されていない" - -#: terminal/serializers/applet.py:54 +#: terminal/serializers/applet_host.py:20 #, fuzzy #| msgid "Session" msgid "Per Session" msgstr "セッション" -#: terminal/serializers/applet.py:55 +#: terminal/serializers/applet_host.py:21 msgid "Per Device" msgstr "" -#: terminal/serializers/applet.py:57 +#: terminal/serializers/applet_host.py:27 #, fuzzy #| msgid "License" msgid "RDS Licensing" msgstr "ライセンス" -#: terminal/serializers/applet.py:58 +#: terminal/serializers/applet_host.py:28 msgid "RDS License Server" msgstr "" -#: terminal/serializers/applet.py:59 +#: terminal/serializers/applet_host.py:29 msgid "RDS Licensing Mode" msgstr "" -#: terminal/serializers/applet.py:60 +#: terminal/serializers/applet_host.py:30 msgid "RDS fSingleSessionPerUser" msgstr "" -#: terminal/serializers/applet.py:61 +#: terminal/serializers/applet_host.py:31 msgid "RDS Max Disconnection Time" msgstr "" -#: terminal/serializers/applet.py:62 +#: terminal/serializers/applet_host.py:32 msgid "RDS Remote App Logoff Time Limit" msgstr "" @@ -5673,7 +5683,7 @@ msgstr "有効期限は開始日より大きくする必要があります" msgid "Permission named `{}` already exists" msgstr "'{}'という名前の権限は既に存在します" -#: tickets/serializers/ticket/ticket.py:99 +#: tickets/serializers/ticket/ticket.py:101 msgid "The ticket flow `{}` does not exist" msgstr "チケットフロー '{}'が存在しない" @@ -6377,10 +6387,6 @@ msgstr "公開鍵をnull、exitに設定することはできません。" msgid "Change auth plan snapshot" msgstr "計画スナップショットの暗号化" -#: xpack/plugins/change_auth_plan/models/base.py:183 -msgid "Ready" -msgstr "の準備を" - #: xpack/plugins/change_auth_plan/models/base.py:184 msgid "Preflight check" msgstr "プリフライトチェック" @@ -6991,11 +6997,11 @@ msgstr "テーマ" msgid "Interface setting" msgstr "インターフェイスの設定" -#: xpack/plugins/license/api.py:50 +#: xpack/plugins/license/api.py:53 msgid "License import successfully" msgstr "ライセンスのインポートに成功" -#: xpack/plugins/license/api.py:51 +#: xpack/plugins/license/api.py:54 msgid "License is invalid" msgstr "ライセンスが無効です" @@ -7019,6 +7025,34 @@ msgstr "究極のエディション" msgid "Community edition" msgstr "コミュニティ版" +#, fuzzy +#~| msgid "Verify auth" +#~ msgid "Account automation" +#~ msgstr "パスワード/キーの確認" + +#~ msgid "The asset {} system platform {} does not support run Ansible tasks" +#~ msgstr "" +#~ "資産 {} システムプラットフォーム {} はAnsibleタスクの実行をサポートしてい" +#~ "ません。" + +#~ msgid "Test assets connectivity: " +#~ msgstr "資産の接続性のテスト:" + +#~ msgid "Unreachable" +#~ msgstr "達成できない" + +#~ msgid "Reachable" +#~ msgstr "接続可能" + +#~ msgid "Get asset info failed: {}" +#~ msgstr "資産情報の取得に失敗しました: {}" + +#~ msgid "Update asset hardware info: " +#~ msgstr "資産ハードウェア情報の更新:" + +#~ msgid "Gather assets users" +#~ msgstr "資産ユーザーの収集" + #, fuzzy #~| msgid "Automatic managed" #~ msgid "Push automation" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 6e116b000..9ba5f0837 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:08529907ac3879f60c2026f91e7ba3f48a3a7d288f7b29cd35c0f73bc3999c21 -size 103630 +oid sha256:0b396cc9a485f6474d14ca30a1a7ba4f954b07754148b964efbb21519c55b280 +size 102849 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 013be87be..131862b1a 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-11-01 15:30+0800\n" +"POT-Creation-Date: 2022-11-03 16:00+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -24,7 +24,7 @@ msgstr "访问控制" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:48 #: applications/models.py:10 assets/models/_user.py:33 #: assets/models/asset/common.py:81 assets/models/asset/common.py:91 -#: assets/models/base.py:59 assets/models/cmd_filter.py:25 +#: assets/models/base.py:57 assets/models/cmd_filter.py:25 #: assets/models/domain.py:24 assets/models/group.py:20 #: assets/models/label.py:17 assets/models/platform.py:22 #: assets/models/platform.py:68 assets/serializers/asset/common.py:86 @@ -59,7 +59,7 @@ msgstr "激活中" #: acls/models/base.py:32 applications/models.py:19 assets/models/_user.py:40 #: assets/models/asset/common.py:100 assets/models/automations/base.py:26 -#: assets/models/backup.py:30 assets/models/base.py:66 +#: assets/models/backup.py:30 assets/models/base.py:65 #: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88 #: assets/models/domain.py:25 assets/models/domain.py:69 #: assets/models/group.py:23 assets/models/label.py:22 @@ -67,7 +67,7 @@ msgstr "激活中" #: ops/models/playbook.py:25 orgs/models.py:74 #: perms/models/asset_permission.py:84 rbac/models/role.py:37 #: settings/models.py:38 terminal/models/applet/applet.py:28 -#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:34 +#: terminal/models/applet/applet.py:61 terminal/models/applet/host.py:104 #: terminal/models/component/endpoint.py:24 #: terminal/models/component/endpoint.py:97 #: terminal/models/component/storage.py:28 @@ -128,14 +128,14 @@ msgstr "审批人" msgid "Login acl" msgstr "登录访问控制" -#: acls/models/login_asset_acl.py:21 assets/models/account.py:57 +#: acls/models/login_asset_acl.py:21 assets/models/account.py:59 #: authentication/models/connection_token.py:33 ops/models/base.py:18 #: terminal/models/session/session.py:34 xpack/plugins/cloud/models.py:87 #: xpack/plugins/cloud/serializers/task.py:65 msgid "Account" msgstr "账号" -#: acls/models/login_asset_acl.py:22 assets/models/account.py:47 +#: acls/models/login_asset_acl.py:22 assets/models/account.py:49 #: assets/models/asset/common.py:83 assets/models/asset/common.py:227 #: assets/models/cmd_filter.py:36 assets/models/gathered_user.py:14 #: assets/serializers/account/account.py:58 assets/serializers/label.py:30 @@ -163,7 +163,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:18 #: acls/serializers/login_asset_acl.py:52 assets/models/_user.py:34 -#: assets/models/base.py:60 assets/models/gathered_user.py:15 +#: assets/models/base.py:58 assets/models/gathered_user.py:15 #: audits/models.py:121 authentication/forms.py:25 authentication/forms.py:27 #: authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 @@ -206,7 +206,7 @@ msgid "" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" #: acls/serializers/login_asset_acl.py:84 -#: tickets/serializers/ticket/ticket.py:85 +#: tickets/serializers/ticket/ticket.py:86 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" @@ -250,7 +250,7 @@ msgstr "类别" #: assets/models/platform.py:70 assets/serializers/asset/common.py:63 #: assets/serializers/platform.py:75 terminal/models/applet/applet.py:24 #: terminal/models/component/storage.py:57 -#: terminal/models/component/storage.py:142 terminal/serializers/applet.py:20 +#: terminal/models/component/storage.py:142 terminal/serializers/applet.py:33 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:17 #: tickets/models/ticket/general.py:273 @@ -296,7 +296,7 @@ msgstr "资产管理" msgid "{} disabled" msgstr "{} 已禁用" -#: assets/const/account.py:6 assets/tasks/const.py:51 audits/const.py:5 +#: assets/const/account.py:6 audits/const.py:5 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" @@ -306,14 +306,14 @@ msgstr "未知" msgid "Ok" msgstr "成功" -#: assets/const/account.py:8 audits/models.py:118 common/const/choices.py:18 +#: assets/const/account.py:8 audits/models.py:118 common/const/choices.py:19 #: xpack/plugins/change_auth_plan/serializers/asset.py:190 #: xpack/plugins/cloud/const.py:33 msgid "Failed" msgstr "失败" #: assets/const/account.py:12 assets/models/_user.py:35 -#: assets/models/base.py:53 assets/models/domain.py:71 +#: assets/models/base.py:52 assets/models/domain.py:71 #: assets/serializers/base.py:15 audits/signal_handlers.py:50 #: authentication/confirm/password.py:9 authentication/forms.py:32 #: authentication/templates/authentication/login.html:228 @@ -330,17 +330,17 @@ msgstr "失败" msgid "Password" msgstr "密码" -#: assets/const/account.py:13 assets/models/base.py:54 +#: assets/const/account.py:13 assets/models/base.py:53 msgid "SSH key" msgstr "SSH 密钥" -#: assets/const/account.py:14 assets/models/base.py:55 +#: assets/const/account.py:14 assets/models/base.py:54 #: authentication/models/access_key.py:31 msgid "Access key" msgstr "Access key" #: assets/const/account.py:15 assets/models/_user.py:38 -#: assets/models/base.py:56 authentication/models/sso_token.py:13 +#: assets/models/base.py:55 authentication/models/sso_token.py:13 msgid "Token" msgstr "Token" @@ -398,7 +398,7 @@ msgid "Replace (The key generated by JumpServer) " msgstr "替换 (由 JumpServer 生成的密钥)" #: assets/const/category.py:11 settings/serializers/auth/radius.py:14 -#: settings/serializers/auth/sms.py:56 terminal/models/applet/applet.py:56 +#: settings/serializers/auth/sms.py:56 terminal/models/applet/applet.py:59 #: terminal/models/component/endpoint.py:12 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" @@ -474,22 +474,20 @@ msgstr "SSH 密钥" msgid "SSH public key" msgstr "SSH 公钥" -#: assets/models/_user.py:41 assets/models/automations/base.py:87 -#: assets/models/base.py:67 assets/models/domain.py:26 -#: assets/models/gathered_user.py:19 assets/models/group.py:22 -#: common/db/models.py:76 common/mixins/models.py:50 ops/models/base.py:53 -#: orgs/models.py:73 perms/models/asset_permission.py:82 +#: assets/models/_user.py:41 assets/models/automations/base.py:96 +#: assets/models/domain.py:26 assets/models/gathered_user.py:19 +#: assets/models/group.py:22 common/db/models.py:76 common/mixins/models.py:50 +#: ops/models/base.py:53 orgs/models.py:73 perms/models/asset_permission.py:82 #: users/models/group.py:18 users/models/user.py:927 msgid "Date created" msgstr "创建日期" -#: assets/models/_user.py:42 assets/models/base.py:68 -#: assets/models/gathered_user.py:20 common/db/models.py:77 -#: common/mixins/models.py:51 +#: assets/models/_user.py:42 assets/models/gathered_user.py:20 +#: common/db/models.py:77 common/mixins/models.py:51 msgid "Date updated" msgstr "更新日期" -#: assets/models/_user.py:43 assets/models/base.py:69 +#: assets/models/_user.py:43 assets/models/base.py:66 #: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:91 #: assets/models/group.py:21 common/db/models.py:74 common/mixins/models.py:49 #: orgs/models.py:71 perms/models/asset_permission.py:81 @@ -557,32 +555,32 @@ msgstr "系统用户" msgid "Can match system user" msgstr "可以匹配系统用户" -#: assets/models/account.py:51 +#: assets/models/account.py:53 msgid "Su from" msgstr "切换自" -#: assets/models/account.py:53 settings/serializers/auth/cas.py:18 +#: assets/models/account.py:55 settings/serializers/auth/cas.py:18 #: terminal/models/applet/applet.py:22 msgid "Version" msgstr "版本" -#: assets/models/account.py:63 +#: assets/models/account.py:65 msgid "Can view asset account secret" msgstr "可以查看资产账号密码" -#: assets/models/account.py:64 +#: assets/models/account.py:66 msgid "Can change asset account secret" msgstr "可以更改资产账号密码" -#: assets/models/account.py:65 +#: assets/models/account.py:67 msgid "Can view asset history account" msgstr "可以查看资产历史账号" -#: assets/models/account.py:66 +#: assets/models/account.py:68 msgid "Can view asset history account secret" msgstr "可以查看资产历史账号密码" -#: assets/models/account.py:89 assets/serializers/account/account.py:13 +#: assets/models/account.py:91 assets/serializers/account/account.py:13 msgid "Account template" msgstr "账号模版" @@ -613,9 +611,9 @@ msgid "Nodes" msgstr "节点" #: assets/models/asset/common.py:98 assets/models/automations/base.py:25 -#: assets/models/cmd_filter.py:39 assets/models/domain.py:70 -#: assets/models/label.py:21 terminal/models/applet/applet.py:25 -#: users/serializers/user.py:147 +#: assets/models/base.py:64 assets/models/cmd_filter.py:39 +#: assets/models/domain.py:70 assets/models/label.py:21 +#: terminal/models/applet/applet.py:25 users/serializers/user.py:147 msgid "Is active" msgstr "激活" @@ -632,7 +630,9 @@ msgid "Can test asset connectivity" msgstr "可以测试资产连接性" #: assets/models/asset/common.py:232 -msgid "Can push system user to asset" +#, fuzzy +#| msgid "Can push system user to asset" +msgid "Can push account to asset" msgstr "可以推送系统用户到资产" #: assets/models/asset/common.py:233 @@ -648,6 +648,7 @@ msgid "Move asset to node" msgstr "移动资产到节点" #: assets/models/asset/web.py:9 audits/models.py:111 +#: terminal/serializers/applet_host.py:24 msgid "Disabled" msgstr "禁用" @@ -688,13 +689,13 @@ msgstr "账号管理" msgid "Assets" msgstr "资产" -#: assets/models/automations/base.py:77 assets/models/automations/base.py:84 +#: assets/models/automations/base.py:86 assets/models/automations/base.py:93 msgid "Automation task" msgstr "自动化任务" -#: assets/models/automations/base.py:88 assets/models/backup.py:77 +#: assets/models/automations/base.py:97 assets/models/backup.py:77 #: audits/models.py:44 ops/models/base.py:54 -#: perms/models/asset_permission.py:76 terminal/models/applet/host.py:32 +#: perms/models/asset_permission.py:76 terminal/models/applet/host.py:102 #: terminal/models/session/session.py:43 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:21 @@ -704,28 +705,28 @@ msgstr "自动化任务" msgid "Date start" msgstr "开始日期" -#: assets/models/automations/base.py:89 +#: assets/models/automations/base.py:98 #: assets/models/automations/change_secret.py:58 ops/models/base.py:55 -#: terminal/models/applet/host.py:33 +#: terminal/models/applet/host.py:103 msgid "Date finished" msgstr "结束日期" -#: assets/models/automations/base.py:91 +#: assets/models/automations/base.py:100 msgid "Automation snapshot" msgstr "自动化快照" -#: assets/models/automations/base.py:95 assets/models/backup.py:88 +#: assets/models/automations/base.py:104 assets/models/backup.py:88 #: assets/serializers/account/backup.py:36 #: xpack/plugins/change_auth_plan/models/base.py:121 #: xpack/plugins/change_auth_plan/serializers/base.py:78 msgid "Trigger mode" msgstr "触发模式" -#: assets/models/automations/base.py:99 +#: assets/models/automations/base.py:108 msgid "Automation task execution" msgstr "自动化任务执行" -#: assets/models/automations/change_secret.py:15 assets/models/base.py:62 +#: assets/models/automations/change_secret.py:15 assets/models/base.py:60 msgid "Secret type" msgstr "密文类型" @@ -734,7 +735,7 @@ msgid "Secret strategy" msgstr "密钥策略" #: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:56 assets/models/base.py:64 +#: assets/models/automations/change_secret.py:56 assets/models/base.py:62 #: assets/serializers/account/base.py:17 #: authentication/models/connection_token.py:34 #: authentication/models/temp_token.py:10 @@ -772,7 +773,7 @@ msgstr "原来密码" msgid "Date started" msgstr "开始日期" -#: assets/models/automations/change_secret.py:60 common/const/choices.py:19 +#: assets/models/automations/change_secret.py:60 common/const/choices.py:20 msgid "Error" msgstr "错误" @@ -794,6 +795,12 @@ msgstr "收集资产信息" msgid "Gather asset facts" msgstr "收集资产信息" +#: assets/models/automations/ping.py:15 +#, fuzzy +#| msgid "Login asset" +msgid "Ping asset" +msgstr "登录资产" + #: assets/models/automations/push_account.py:16 #, fuzzy #| msgid "Is service account" @@ -841,15 +848,15 @@ msgstr "是否成功" msgid "Account backup execution" msgstr "账号备份执行" -#: assets/models/base.py:30 assets/serializers/domain.py:42 +#: assets/models/base.py:29 assets/serializers/domain.py:42 msgid "Connectivity" msgstr "可连接性" -#: assets/models/base.py:32 authentication/models/temp_token.py:12 +#: assets/models/base.py:31 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "校验日期" -#: assets/models/base.py:65 +#: assets/models/base.py:63 msgid "Privileged" msgstr "特权的" @@ -1015,6 +1022,7 @@ msgid "Setting" msgstr "设置" #: assets/models/platform.py:43 audits/models.py:112 settings/models.py:37 +#: terminal/serializers/applet_host.py:25 msgid "Enabled" msgstr "启用" @@ -1306,63 +1314,41 @@ msgstr "密码不能包含 `'` 字符" msgid "Password can not contains `\"` " msgstr "密码不能包含 `\"` 字符" -#: assets/tasks/account_connectivity.py:30 -msgid "The asset {} system platform {} does not support run Ansible tasks" -msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" - -#: assets/tasks/account_connectivity.py:108 -msgid "Test account connectivity: " -msgstr "测试账号可连接性: " - -#: assets/tasks/asset_connectivity.py:49 -msgid "Test assets connectivity. " -msgstr "测试资产可连接性. " - -#: assets/tasks/asset_connectivity.py:94 assets/tasks/asset_connectivity.py:107 -msgid "Test assets connectivity: " -msgstr "测试资产可连接性: " - -#: assets/tasks/asset_connectivity.py:121 -msgid "Test if the assets under the node are connectable: " -msgstr "测试节点下资产是否可连接: " - -#: assets/tasks/const.py:49 -msgid "Unreachable" -msgstr "不可达" - -#: assets/tasks/const.py:50 -msgid "Reachable" -msgstr "可连接" - -#: assets/tasks/gather_asset_hardware_info.py:46 -msgid "Get asset info failed: {}" -msgstr "获取资产信息失败:{}" - -#: assets/tasks/gather_asset_hardware_info.py:97 +#: assets/tasks/gather_facts.py:25 msgid "Update some assets hardware info. " msgstr "更新资产硬件信息. " -#: assets/tasks/gather_asset_hardware_info.py:118 -msgid "Update asset hardware info: " -msgstr "更新资产硬件信息: " - -#: assets/tasks/gather_asset_hardware_info.py:124 +#: assets/tasks/gather_facts.py:48 msgid "Update assets hardware info: " msgstr "更新资产硬件信息: " -#: assets/tasks/gather_asset_hardware_info.py:146 +#: assets/tasks/gather_facts.py:58 msgid "Update node asset hardware information: " msgstr "更新节点资产硬件信息: " -#: assets/tasks/gather_asset_users.py:110 -msgid "Gather assets users" -msgstr "收集资产上的用户" - #: assets/tasks/nodes_amount.py:29 msgid "" "The task of self-checking is already running and cannot be started repeatedly" msgstr "自检程序已经在运行,不能重复启动" +#: assets/tasks/ping.py:20 assets/tasks/ping.py:38 +#, fuzzy +#| msgid "Test assets connectivity. " +msgid "Test assets connectivity " +msgstr "测试资产可连接性. " + +#: assets/tasks/ping.py:48 +#, fuzzy +#| msgid "Test if the assets under the node are connectable: " +msgid "Test if the assets under the node are connectable " +msgstr "测试节点下资产是否可连接: " + +#: assets/tasks/push_account.py:36 +#, fuzzy +#| msgid "Push account method" +msgid "Push accounts to assets" +msgstr "推送方式" + #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "资产已经被禁用, 跳过: {}" @@ -1379,6 +1365,12 @@ msgstr "为了安全,禁止推送用户 {}" msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" +#: assets/tasks/verify_account.py:36 +#, fuzzy +#| msgid "Test account connectivity: " +msgid "Verify accounts connectivity" +msgstr "测试账号可连接性: " + #: audits/apps.py:9 msgid "Audits" msgstr "日志审计" @@ -1426,7 +1418,7 @@ msgstr "操作" msgid "Filename" msgstr "文件名" -#: audits/models.py:43 audits/models.py:117 common/const/choices.py:17 +#: audits/models.py:43 audits/models.py:117 common/const/choices.py:18 #: terminal/models/session/sharing.py:104 tickets/views/approve.py:114 #: xpack/plugins/change_auth_plan/serializers/asset.py:189 msgid "Success" @@ -1506,8 +1498,8 @@ msgid "MFA" msgstr "MFA" #: audits/models.py:128 ops/models/base.py:48 -#: terminal/models/applet/applet.py:57 terminal/models/applet/host.py:19 -#: terminal/models/applet/host.py:31 terminal/models/component/status.py:33 +#: terminal/models/applet/applet.py:60 terminal/models/applet/host.py:101 +#: terminal/models/component/status.py:33 terminal/serializers/applet.py:22 #: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:171 #: xpack/plugins/cloud/models.py:223 msgid "Status" @@ -2018,11 +2010,11 @@ msgstr "资产未激活" msgid "No account" msgstr "登录账号" -#: authentication/models/connection_token.py:103 +#: authentication/models/connection_token.py:101 msgid "User has no permission to access asset or permission expired" msgstr "用户没有权限访问资产或权限已过期" -#: authentication/models/connection_token.py:145 +#: authentication/models/connection_token.py:144 msgid "Super connection token" msgstr "超级连接令牌" @@ -2439,15 +2431,19 @@ msgstr "手动触发" msgid "Timing trigger" msgstr "定时触发" -#: common/const/choices.py:15 tickets/const.py:29 tickets/const.py:37 +#: common/const/choices.py:15 xpack/plugins/change_auth_plan/models/base.py:183 +msgid "Ready" +msgstr "准备" + +#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:37 msgid "Pending" msgstr "待定的" -#: common/const/choices.py:16 +#: common/const/choices.py:17 msgid "Running" msgstr "" -#: common/const/choices.py:20 +#: common/const/choices.py:21 #, fuzzy #| msgid "Cancel" msgid "Canceled" @@ -2515,6 +2511,12 @@ msgstr "解析文件错误: {}" msgid "Children" msgstr "" +#: common/drf/serializers/common.py:94 +#, fuzzy +#| msgid "Filename" +msgid "File" +msgstr "文件名" + #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -2925,7 +2927,7 @@ msgstr "组织管理" #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:88 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 -#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71 +#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:72 msgid "Organization" msgstr "组织" @@ -4725,42 +4727,46 @@ msgstr "作者" msgid "Tags" msgstr "标签" -#: terminal/models/applet/applet.py:29 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:31 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "主机" -#: terminal/models/applet/applet.py:55 terminal/models/applet/host.py:21 +#: terminal/models/applet/applet.py:58 terminal/models/applet/host.py:28 msgid "Applet" msgstr "远程应用" -#: terminal/models/applet/host.py:14 -msgid "Account automation" -msgstr "账号自动化" - -#: terminal/models/applet/host.py:15 terminal/serializers/applet.py:66 +#: terminal/models/applet/host.py:19 terminal/serializers/applet_host.py:36 #, fuzzy #| msgid "More login options" msgid "Deploy options" msgstr "其他方式登录" -#: terminal/models/applet/host.py:16 +#: terminal/models/applet/host.py:20 msgid "Inited" msgstr "" -#: terminal/models/applet/host.py:17 +#: terminal/models/applet/host.py:21 #, fuzzy #| msgid "Date finished" msgid "Date inited" msgstr "结束日期" -#: terminal/models/applet/host.py:18 +#: terminal/models/applet/host.py:22 msgid "Date synced" msgstr "最后同步日期" -#: terminal/models/applet/host.py:30 +#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:183 +msgid "Terminal" +msgstr "终端" + +#: terminal/models/applet/host.py:99 msgid "Hosting" msgstr "主机" +#: terminal/models/applet/host.py:100 +msgid "Initial" +msgstr "" + #: terminal/models/component/endpoint.py:14 msgid "HTTPS Port" msgstr "HTTPS 端口" @@ -4865,10 +4871,6 @@ msgstr "录像存储" msgid "type" msgstr "类型" -#: terminal/models/component/terminal.py:183 -msgid "Terminal" -msgstr "终端" - #: terminal/models/component/terminal.py:185 msgid "Can view terminal config" msgstr "可以查看终端配置" @@ -4985,43 +4987,51 @@ msgstr "级别" msgid "Batch danger command alert" msgstr "批量危险命令告警" -#: terminal/serializers/applet.py:19 +#: 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 msgid "Icon" msgstr "图标" -#: terminal/serializers/applet.py:53 -msgid "Not set" -msgstr "不设置" - -#: terminal/serializers/applet.py:54 +#: terminal/serializers/applet_host.py:20 msgid "Per Session" msgstr "按会话" -#: terminal/serializers/applet.py:55 +#: terminal/serializers/applet_host.py:21 msgid "Per Device" msgstr "按设备" -#: terminal/serializers/applet.py:57 +#: terminal/serializers/applet_host.py:27 msgid "RDS Licensing" msgstr "部署 RDS 许可服务" -#: terminal/serializers/applet.py:58 +#: terminal/serializers/applet_host.py:28 msgid "RDS License Server" msgstr "RDS 许可服务主机" -#: terminal/serializers/applet.py:59 +#: terminal/serializers/applet_host.py:29 msgid "RDS Licensing Mode" msgstr "RDS 许可模式" -#: terminal/serializers/applet.py:60 +#: terminal/serializers/applet_host.py:30 msgid "RDS fSingleSessionPerUser" msgstr "RDS 会话用户数" -#: terminal/serializers/applet.py:61 +#: terminal/serializers/applet_host.py:31 msgid "RDS Max Disconnection Time" msgstr "RDS 会话断开时间" -#: terminal/serializers/applet.py:62 +#: terminal/serializers/applet_host.py:32 msgid "RDS Remote App Logoff Time Limit" msgstr "RDS 远程应用注销时间" @@ -5447,7 +5457,7 @@ msgstr "过期时间要大于开始时间" msgid "Permission named `{}` already exists" msgstr "授权名称 `{}` 已存在" -#: tickets/serializers/ticket/ticket.py:99 +#: tickets/serializers/ticket/ticket.py:101 msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" @@ -6136,10 +6146,6 @@ msgstr "公钥不能设置为空, 退出. " msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models/base.py:183 -msgid "Ready" -msgstr "准备" - #: xpack/plugins/change_auth_plan/models/base.py:184 msgid "Preflight check" msgstr "改密前的校验" @@ -6747,11 +6753,11 @@ msgstr "主题" msgid "Interface setting" msgstr "界面设置" -#: xpack/plugins/license/api.py:50 +#: xpack/plugins/license/api.py:53 msgid "License import successfully" msgstr "许可证导入成功" -#: xpack/plugins/license/api.py:51 +#: xpack/plugins/license/api.py:54 msgid "License is invalid" msgstr "无效的许可证" @@ -6775,6 +6781,30 @@ msgstr "旗舰版" msgid "Community edition" msgstr "社区版" +#~ msgid "Account automation" +#~ msgstr "账号自动化" + +#~ msgid "The asset {} system platform {} does not support run Ansible tasks" +#~ msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" + +#~ msgid "Test assets connectivity: " +#~ msgstr "测试资产可连接性: " + +#~ msgid "Unreachable" +#~ msgstr "不可达" + +#~ msgid "Reachable" +#~ msgstr "可连接" + +#~ msgid "Get asset info failed: {}" +#~ msgstr "获取资产信息失败:{}" + +#~ msgid "Update asset hardware info: " +#~ msgstr "更新资产硬件信息: " + +#~ msgid "Gather assets users" +#~ msgstr "收集资产上的用户" + #~ msgid "Push automation" #~ msgstr "自动化推送" diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index 4a627e451..a00814dd3 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -13,9 +13,9 @@ from rest_framework.response import Response from rest_framework.serializers import ValidationError from common.utils import is_uuid +from common.drf.serializers import FileSerializer from terminal import serializers from terminal.models import AppletPublication, Applet -from terminal.serializers import AppletUploadSerializer __all__ = ['AppletViewSet', 'AppletPublicationViewSet'] @@ -59,7 +59,7 @@ class DownloadUploadMixin: raise ValidationError({'error': 'Missing name in manifest.yml'}) return manifest, tmp_dir - @action(detail=False, methods=['post'], serializer_class=AppletUploadSerializer) + @action(detail=False, methods=['post'], serializer_class=FileSerializer) def upload(self, request, *args, **kwargs): manifest, tmp_dir = self.extract_and_check_file(request) name = manifest['name'] diff --git a/apps/terminal/api/applet/host.py b/apps/terminal/api/applet/host.py index e5dee8c7f..f434b5b89 100644 --- a/apps/terminal/api/applet/host.py +++ b/apps/terminal/api/applet/host.py @@ -2,6 +2,8 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response +from common.drf.api import JMSModelViewSet +from orgs.utils import tmp_to_builtin_org from terminal import serializers from terminal.models import AppletHost, Applet, AppletHostDeployment from terminal.tasks import run_applet_host_deployment @@ -10,9 +12,34 @@ from terminal.tasks import run_applet_host_deployment __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet'] -class AppletHostViewSet(viewsets.ModelViewSet): +class AppletHostViewSet(JMSModelViewSet): serializer_class = serializers.AppletHostSerializer queryset = AppletHost.objects.all() + rbac_perms = { + 'accounts': 'terminal.view_applethost', + 'reports': '*' + } + + @action(methods=['post'], detail=True, serializer_class=serializers.AppletHostReportSerializer) + def reports(self, request, *args, **kwargs): + # 1. Host 和 Terminal 关联 + # 2. 上报 安装的 Applets 每小时 + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + data = serializer.validated_data + instance.check_terminal_binding(request) + instance.check_applets_state(data['applets']) + return Response({'msg': 'ok'}) + + @action(methods=['get'], detail=True, serializer_class=serializers.AppletHostAccountSerializer) + def accounts(self, request, *args, **kwargs): + host = self.get_object() + with tmp_to_builtin_org(system=1): + accounts = host.accounts.all().filter(privileged=False) + response = self.get_paginated_response_from_queryset(accounts) + return response class AppletHostDeploymentViewSet(viewsets.ModelViewSet): diff --git a/apps/terminal/api/component/status.py b/apps/terminal/api/component/status.py index 5e27926ea..4c4e31f27 100644 --- a/apps/terminal/api/component/status.py +++ b/apps/terminal/api/component/status.py @@ -21,7 +21,7 @@ __all__ = ['StatusViewSet', 'ComponentsMetricsAPIView'] class StatusViewSet(viewsets.ModelViewSet): queryset = Status.objects.all() - serializer_class = serializers.StatusSerializer + serializer_class = serializers.StatSerializer session_serializer_class = serializers.SessionSerializer task_serializer_class = serializers.TaskSerializer diff --git a/apps/terminal/api/component/storage.py b/apps/terminal/api/component/storage.py index d46b6f91f..c912e3131 100644 --- a/apps/terminal/api/component/storage.py +++ b/apps/terminal/api/component/storage.py @@ -61,7 +61,7 @@ class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet): if not filterset.is_valid(): raise utils.translate_validation(filterset.errors) command_qs = filterset.qs - if storage.type == const.CommandStorageTypeChoices.es: + if storage.type == const.CommandStorageType.es: command_count = command_qs.count(limit_to_max_result_window=False) else: command_count = command_qs.count() diff --git a/apps/terminal/api/component/terminal.py b/apps/terminal/api/component/terminal.py index a58181bf4..3133db6eb 100644 --- a/apps/terminal/api/component/terminal.py +++ b/apps/terminal/api/component/terminal.py @@ -42,43 +42,12 @@ class TerminalViewSet(JMSBulkModelViewSet): self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) - def create(self, request, *args, **kwargs): - if isinstance(request.data, list): - raise exceptions.BulkCreateNotSupport() - - name = request.data.get('name') - remote_ip = request.META.get('REMOTE_ADDR') - x_real_ip = request.META.get('X-Real-IP') - remote_addr = x_real_ip or remote_ip - - terminal = get_object_or_none(Terminal, name=name, is_deleted=False) - if terminal: - msg = 'Terminal name %s already used' % name - return Response({'msg': msg}, status=409) - - serializer = self.serializer_class(data={ - 'name': name, 'remote_addr': remote_addr - }) - - if serializer.is_valid(): - terminal = serializer.save() - - # App should use id, token get access key, if accepted - token = uuid.uuid4().hex - cache.set(token, str(terminal.id), 3600) - data = {"id": str(terminal.id), "token": token, "msg": "Need accept"} - return Response(data, status=201) - else: - data = serializer.errors - logger.error("Register terminal error: {}".format(data)) - return Response(data, status=400) - def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) s = self.request.query_params.get('status') if not s: return queryset - filtered_queryset_id = [str(q.id) for q in queryset if q.latest_status == s] + filtered_queryset_id = [str(q.id) for q in queryset if q.load == s] queryset = queryset.filter(id__in=filtered_queryset_id) return queryset diff --git a/apps/terminal/automations/deploy_applet_host/__init__.py b/apps/terminal/automations/deploy_applet_host/__init__.py index c02aa491a..8b415ece4 100644 --- a/apps/terminal/automations/deploy_applet_host/__init__.py +++ b/apps/terminal/automations/deploy_applet_host/__init__.py @@ -27,13 +27,19 @@ class DeployAppletHostManager: def generate_playbook(self): playbook_src = os.path.join(CURRENT_DIR, 'playbook.yml') + base_site_url = settings.BASE_SITE_URL + bootstrap_token = settings.BOOTSTRAP_TOKEN + host_id = str(self.deployment.host.id) + if not base_site_url: + base_site_url = "localhost:8080" with open(playbook_src) as f: plays = yaml.safe_load(f) for play in plays: play['vars'].update(self.deployment.host.deploy_options) - play['vars']['DownloadHost'] = settings.BASE_URL + '/download/' - play['vars']['CORE_HOST'] = settings.BASE_URL - play['vars']['BOOTSTRAP_TOKEN'] = settings.BOOSTRAP_TOKEN + play['vars']['DownloadHost'] = base_site_url + '/download/' + play['vars']['CORE_HOST'] = base_site_url + play['vars']['BOOTSTRAP_TOKEN'] = bootstrap_token + play['vars']['HOST_ID'] = host_id play['vars']['HOST_NAME'] = self.deployment.host.name playbook_dir = os.path.join(self.run_dir, 'playbook') @@ -70,6 +76,3 @@ class DeployAppletHostManager: self.deployment.date_finished = timezone.now() with safe_db_connection(): self.deployment.save() - - - diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index b2c64f0cb..867d58f76 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -5,6 +5,7 @@ DownloadHost: https://demo.jumpserver.org/download Initial: 0 HOST_NAME: test + HOST_ID: 00000000-0000-0000-0000-000000000000 CORE_HOST: https://demo.jumpserver.org BOOTSTRAP_TOKEN: PleaseChangeMe RDS_Licensing: true @@ -38,7 +39,7 @@ - name: Install JumpServer Remoteapp agent (jumpserver) ansible.windows.win_package: path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" - args: + arguments: - /VERYSILENT - /SUPPRESSMSGBOXES - /NORESTART @@ -154,7 +155,8 @@ - name: Generate component config ansible.windows.win_shell: - "remoteapp-server config --core_host {{ CORE_HOST }} --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}" + "remoteapp-server config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} + --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}" - name: Install remoteapp-server service ansible.windows.win_shell: diff --git a/apps/terminal/const.py b/apps/terminal/const.py index ef23030df..acea15238 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ # -------------------------------- -class ReplayStorageTypeChoices(TextChoices): +class ReplayStorageType(TextChoices): null = 'null', 'Null', server = 'server', 'Server' s3 = 's3', 'S3' @@ -20,7 +20,7 @@ class ReplayStorageTypeChoices(TextChoices): cos = 'cos', 'COS' -class CommandStorageTypeChoices(TextChoices): +class CommandStorageType(TextChoices): null = 'null', 'Null', server = 'server', 'Server' es = 'es', 'Elasticsearch' @@ -29,7 +29,7 @@ class CommandStorageTypeChoices(TextChoices): # Component Status Choices # ------------------------ -class ComponentStatusChoices(TextChoices): +class ComponentLoad(TextChoices): critical = 'critical', _('Critical') high = 'high', _('High') normal = 'normal', _('Normal') @@ -40,7 +40,7 @@ class ComponentStatusChoices(TextChoices): return set(dict(cls.choices).keys()) -class TerminalTypeChoices(TextChoices): +class TerminalType(TextChoices): koko = 'koko', 'KoKo' guacamole = 'guacamole', 'Guacamole' omnidb = 'omnidb', 'OmniDB' @@ -50,7 +50,7 @@ class TerminalTypeChoices(TextChoices): celery = 'celery', 'Celery' magnus = 'magnus', 'Magnus' razor = 'razor', 'Razor' - tinker = 'tinker', 'Tinker' + tinker = 'tinker', 'Tinker' @classmethod def types(cls): diff --git a/apps/terminal/migrations/0050_auto_20220606_1745.py b/apps/terminal/migrations/0050_auto_20220606_1745.py index 88e7cc138..e88d37971 100644 --- a/apps/terminal/migrations/0050_auto_20220606_1745.py +++ b/apps/terminal/migrations/0050_auto_20220606_1745.py @@ -13,6 +13,10 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='terminal', name='type', - field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), ('razor', 'Razor')], default='koko', max_length=64, verbose_name='type'), + field=models.CharField(choices=[ + ('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), + ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), + ('magnus', 'Magnus'), ('razor', 'Razor'), ('tinker', 'Tinker'), + ], default='koko', max_length=64, verbose_name='type'), ), ] diff --git a/apps/terminal/migrations/0054_auto_20221027_1125.py b/apps/terminal/migrations/0054_auto_20221027_1125.py index 9a3bf4b85..9fde61be2 100644 --- a/apps/terminal/migrations/0054_auto_20221027_1125.py +++ b/apps/terminal/migrations/0054_auto_20221027_1125.py @@ -39,7 +39,6 @@ class Migration(migrations.Migration): 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')), - ('account_automation', models.BooleanField(default=False, verbose_name='Account automation')), ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), ('status', models.CharField(max_length=16, verbose_name='Status')), ], diff --git a/apps/terminal/migrations/0057_auto_20221102_1941.py b/apps/terminal/migrations/0057_auto_20221102_1941.py new file mode 100644 index 000000000..56f1e699a --- /dev/null +++ b/apps/terminal/migrations/0057_auto_20221102_1941.py @@ -0,0 +1,24 @@ +# 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/0058_auto_20221103_1624.py b/apps/terminal/migrations/0058_auto_20221103_1624.py new file mode 100644 index 000000000..0c8091e5c --- /dev/null +++ b/apps/terminal/migrations/0058_auto_20221103_1624.py @@ -0,0 +1,33 @@ +# 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/models/applet/host.py b/apps/terminal/models/applet/host.py index 9c2338591..073ccc1fb 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -1,7 +1,14 @@ +import os +from collections import defaultdict + from django.db import models from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from rest_framework.exceptions import ValidationError +from simple_history.utils import bulk_create_with_history from common.db.models import JMSBaseModel +from common.utils import random_string from assets.models import Host @@ -9,22 +16,90 @@ __all__ = ['AppletHost', 'AppletHostDeployment'] class AppletHost(Host): - LOCKING_ORG = '00000000-0000-0000-0000-000000000004' - - account_automation = models.BooleanField(default=False, verbose_name=_('Account automation')) deploy_options = models.JSONField(default=dict, verbose_name=_('Deploy options')) inited = models.BooleanField(default=False, verbose_name=_('Inited')) date_inited = models.DateTimeField(null=True, blank=True, verbose_name=_('Date inited')) date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced')) - status = models.CharField(max_length=16, verbose_name=_('Status')) + terminal = models.OneToOneField( + 'terminal.Terminal', on_delete=models.PROTECT, null=True, blank=True, + related_name='applet_host', verbose_name=_('Terminal') + ) applets = models.ManyToManyField( 'Applet', verbose_name=_('Applet'), through='AppletPublication', through_fields=('host', 'applet'), ) + LOCKING_ORG = '00000000-0000-0000-0000-000000000004' def __str__(self): return self.name + @property + def status(self): + if self.terminal: + return 'online' + return self.terminal.status + + def check_terminal_binding(self, request): + request_terminal = getattr(request.user, 'terminal', None) + if not request_terminal: + raise ValidationError('Request user has no terminal') + + self.date_synced = timezone.now() + if not self.terminal: + self.terminal = request_terminal + self.save(update_fields=['terminal', 'date_synced']) + elif self.terminal and self.terminal != request_terminal: + raise ValidationError('Terminal has been set') + else: + self.save(update_fields=['date_synced']) + + def check_applets_state(self, applets_value_list): + applets = self.applets.all() + name_version_mapper = { + value['name']: value['version'] + for value in applets_value_list + } + + status_applets = defaultdict(list) + for applet in applets: + if applet.name not in name_version_mapper: + status_applets['unpublished'].append(applet) + elif applet.version != name_version_mapper[applet.name]: + status_applets['not_match'].append(applet) + else: + status_applets['published'].append(applet) + + for status, applets in status_applets.items(): + self.publications.filter(applet__in=applets)\ + .exclude(status=status)\ + .update(status=status) + + @staticmethod + def random_username(): + return 'jms_' + random_string(8) + + @staticmethod + def random_password(): + return random_string(16, special_char=True) + + def generate_accounts(self): + amount = int(os.getenv('TERMINAL_ACCOUNTS_AMOUNT', 100)) + now_count = self.accounts.filter(privileged=False).count() + need = amount - now_count + + accounts = [] + account_model = self.accounts.model + for i in range(need): + username = self.random_username() + password = self.random_password() + account = account_model( + username=username, secret=password, name=username, + asset_id=self.id, secret_type='password', version=1, + org_id=self.LOCKING_ORG + ) + accounts.append(account) + bulk_create_with_history(accounts, account_model, batch_size=20) + class AppletHostDeployment(JMSBaseModel): host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting')) diff --git a/apps/terminal/models/component/status.py b/apps/terminal/models/component/status.py index 1ecfc0e2a..3da13d9f0 100644 --- a/apps/terminal/models/component/status.py +++ b/apps/terminal/models/component/status.py @@ -1,10 +1,6 @@ -from __future__ import unicode_literals - import uuid from django.db import models -from django.forms.models import model_to_dict -from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger @@ -25,53 +21,9 @@ class Status(models.Model): terminal = models.ForeignKey('terminal.Terminal', null=True, on_delete=models.CASCADE) date_created = models.DateTimeField(auto_now_add=True) - CACHE_KEY = 'TERMINAL_STATUS_{}' - class Meta: db_table = 'terminal_status' get_latest_by = 'date_created' verbose_name = _("Status") - def save_to_cache(self): - if not self.terminal: - return - key = self.CACHE_KEY.format(self.terminal.id) - data = model_to_dict(self) - cache.set(key, data, 60*3) - return data - - @classmethod - def get_terminal_latest_status(cls, terminal): - from ...utils import ComputeStatUtil - stat = cls.get_terminal_latest_stat(terminal) - return ComputeStatUtil.compute_component_status(stat) - - @classmethod - def get_terminal_latest_stat(cls, terminal): - key = cls.CACHE_KEY.format(terminal.id) - data = cache.get(key) - if not data: - return None - data.pop('terminal', None) - stat = cls(**data) - stat.terminal = terminal - stat.is_alive = terminal.is_alive - stat.keep_one_decimal_place() - return stat - - def keep_one_decimal_place(self): - keys = ['cpu_load', 'memory_used', 'disk_used'] - for key in keys: - value = getattr(self, key, 0) - if not isinstance(value, (int, float)): - continue - value = '%.1f' % value - setattr(self, key, float(value)) - - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - self.terminal.set_alive(ttl=120) - return self.save_to_cache() - # return super().save() - diff --git a/apps/terminal/models/component/storage.py b/apps/terminal/models/component/storage.py index 7cab98058..11a4849a6 100644 --- a/apps/terminal/models/component/storage.py +++ b/apps/terminal/models/component/storage.py @@ -53,21 +53,21 @@ class CommonStorageModelMixin(models.Model): class CommandStorage(CommonStorageModelMixin, CommonModelMixin): type = models.CharField( - max_length=16, choices=const.CommandStorageTypeChoices.choices, - default=const.CommandStorageTypeChoices.server.value, verbose_name=_('Type'), + max_length=16, choices=const.CommandStorageType.choices, + default=const.CommandStorageType.server.value, verbose_name=_('Type'), ) @property def type_null(self): - return self.type == const.CommandStorageTypeChoices.null.value + return self.type == const.CommandStorageType.null.value @property def type_server(self): - return self.type == const.CommandStorageTypeChoices.server.value + return self.type == const.CommandStorageType.server.value @property def type_es(self): - return self.type == const.CommandStorageTypeChoices.es.value + return self.type == const.CommandStorageType.es.value @property def type_null_or_server(self): @@ -138,17 +138,17 @@ class CommandStorage(CommonStorageModelMixin, CommonModelMixin): class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): type = models.CharField( - max_length=16, choices=const.ReplayStorageTypeChoices.choices, - default=const.ReplayStorageTypeChoices.server.value, verbose_name=_('Type') + max_length=16, choices=const.ReplayStorageType.choices, + default=const.ReplayStorageType.server.value, verbose_name=_('Type') ) @property def type_null(self): - return self.type == const.ReplayStorageTypeChoices.null.value + return self.type == const.ReplayStorageType.null.value @property def type_server(self): - return self.type == const.ReplayStorageTypeChoices.server.value + return self.type == const.ReplayStorageType.server.value @property def type_null_or_server(self): @@ -156,11 +156,11 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): @property def type_swift(self): - return self.type == const.ReplayStorageTypeChoices.swift.value + return self.type == const.ReplayStorageType.swift.value @property def type_ceph(self): - return self.type == const.ReplayStorageTypeChoices.ceph.value + return self.type == const.ReplayStorageType.ceph.value @property def config(self): @@ -168,7 +168,7 @@ class ReplayStorage(CommonStorageModelMixin, CommonModelMixin): # add type config if self.type_ceph: - _type = const.ReplayStorageTypeChoices.s3.value + _type = const.ReplayStorageType.s3.value else: _type = self.type _config.update({'TYPE': _type}) diff --git a/apps/terminal/models/component/terminal.py b/apps/terminal/models/component/terminal.py index 4a95fbd3a..eb68915e4 100644 --- a/apps/terminal/models/component/terminal.py +++ b/apps/terminal/models/component/terminal.py @@ -1,16 +1,15 @@ import uuid +from django.utils import timezone from django.db import models from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from common.utils import get_logger +from common.utils import get_logger, lazyproperty from users.models import User from orgs.utils import tmp_to_root_org -from .status import Status -from terminal import const -from terminal.const import ComponentStatusChoices as StatusChoice +from terminal.const import TerminalType as TypeChoices, ComponentLoad as StatusChoice from ..session import Session @@ -18,42 +17,24 @@ logger = get_logger(__file__) class TerminalStatusMixin: - ALIVE_KEY = 'TERMINAL_ALIVE_{}' id: str + ALIVE_KEY = 'TERMINAL_ALIVE_{}' + status_set: models.Manager - @property - def latest_status(self): - return Status.get_terminal_latest_status(self) + @lazyproperty + def last_stat(self): + return self.status_set.order_by('date_created').last() - @property - def latest_status_display(self): - return self.latest_status.label - - @property - def latest_stat(self): - return Status.get_terminal_latest_stat(self) - - @property - def is_normal(self): - return self.latest_status == StatusChoice.normal - - @property - def is_high(self): - return self.latest_status == StatusChoice.high - - @property - def is_critical(self): - return self.latest_status == StatusChoice.critical + @lazyproperty + def load(self): + from ...utils import ComputeLoadUtil + return ComputeLoadUtil.compute_load(self.last_stat) @property def is_alive(self): - key = self.ALIVE_KEY.format(self.id) - # return self.latest_status != StatusChoice.offline - return cache.get(key, False) - - def set_alive(self, ttl=120): - key = self.ALIVE_KEY.format(self.id) - cache.set(key, True, ttl) + if not self.last_stat: + return False + return self.last_stat.date_created > timezone.now() - timezone.timedelta(seconds=120) class StorageMixin: @@ -99,16 +80,13 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) type = models.CharField( - choices=const.TerminalTypeChoices.choices, default=const.TerminalTypeChoices.koko.value, + choices=TypeChoices.choices, default=TypeChoices.koko, max_length=64, verbose_name=_('type') ) remote_addr = models.CharField(max_length=128, blank=True, verbose_name=_('Remote Address')) - ssh_port = models.IntegerField(verbose_name=_('SSH Port'), default=2222) - http_port = models.IntegerField(verbose_name=_('HTTP Port'), default=5000) command_storage = models.CharField(max_length=128, verbose_name=_("Command storage"), default='default') replay_storage = models.CharField(max_length=128, verbose_name=_("Replay storage"), default='default') user = models.OneToOneField(User, related_name='terminal', verbose_name='Application User', null=True, on_delete=models.CASCADE) - is_accepted = models.BooleanField(default=False, verbose_name='Is Accepted') is_deleted = models.BooleanField(default=False) date_created = models.DateTimeField(auto_now_add=True) comment = models.TextField(blank=True, verbose_name=_('Comment')) @@ -167,9 +145,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): def __str__(self): status = "Active" - if not self.is_accepted: - status = "NotAccept" - elif self.is_deleted: + if self.is_deleted: status = "Deleted" elif not self.is_active: status = "Disable" @@ -178,7 +154,6 @@ class Terminal(StorageMixin, TerminalStatusMixin, models.Model): return '%s: %s' % (self.name, status) class Meta: - ordering = ('is_accepted',) db_table = "terminal" verbose_name = _("Terminal") permissions = ( diff --git a/apps/terminal/serializers/applet.py b/apps/terminal/serializers/applet.py index 183a34c93..b5aa4db68 100644 --- a/apps/terminal/serializers/applet.py +++ b/apps/terminal/serializers/applet.py @@ -1,22 +1,22 @@ from rest_framework import serializers from django.utils.translation import gettext_lazy as _ +from django.db import models from common.drf.fields import ObjectRelatedField, LabeledChoiceField -from common.const.choices import Status from ..models import Applet, AppletPublication, AppletHost __all__ = [ 'AppletSerializer', 'AppletPublicationSerializer', - 'AppletUploadSerializer', ] -class AppletUploadSerializer(serializers.Serializer): - file = serializers.FileField() - - class AppletPublicationSerializer(serializers.ModelSerializer): + class Status(models.TextChoices): + PUBLISHED = 'published', _('Published') + UNPUBLISHED = 'unpublished', _('Unpublished') + NOT_MATCH = 'not_match', _('Not match') + applet = ObjectRelatedField(attrs=('id', 'display_name', 'icon', 'version'), queryset=Applet.objects.all()) host = ObjectRelatedField(queryset=AppletHost.objects.all()) status = LabeledChoiceField(choices=Status.choices, label=_("Status")) diff --git a/apps/terminal/serializers/applet_host.py b/apps/terminal/serializers/applet_host.py index b87ee78d0..10ca442c1 100644 --- a/apps/terminal/serializers/applet_host.py +++ b/apps/terminal/serializers/applet_host.py @@ -2,7 +2,8 @@ from rest_framework import serializers from django.utils.translation import gettext_lazy as _ from common.validators import ProjectUniqueValidator -from assets.models import Platform +from common.drf.fields import ObjectRelatedField +from assets.models import Platform, Account from assets.serializers import HostSerializer from ..models import AppletHost, AppletHostDeployment, Applet from .applet import AppletSerializer @@ -10,6 +11,7 @@ from .applet import AppletSerializer __all__ = [ 'AppletHostSerializer', 'AppletHostDeploymentSerializer', + 'AppletHostAccountSerializer', 'AppletHostReportSerializer' ] @@ -36,7 +38,7 @@ class AppletHostSerializer(HostSerializer): class Meta(HostSerializer.Meta): model = AppletHost fields = HostSerializer.Meta.fields + [ - 'account_automation', 'status', 'date_synced', 'deploy_options' + 'status', 'date_synced', 'deploy_options' ] extra_kwargs = { 'status': {'read_only': True}, @@ -86,3 +88,13 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer): 'date_start', 'date_finished' ] fields = fields_mini + ['comment'] + read_only_fields + + +class AppletHostAccountSerializer(serializers.ModelSerializer): + class Meta: + model = Account + fields = ['id', 'username', 'secret', 'date_updated'] + + +class AppletHostReportSerializer(serializers.Serializer): + applets = ObjectRelatedField(attrs=('id', 'name', 'version'), queryset=Applet.objects.all(), many=True) diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index ff7d386de..cc4478c2d 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -118,13 +118,13 @@ class ReplayStorageTypeAzureSerializer(serializers.Serializer): # mapping replay_storage_type_serializer_classes_mapping = { - const.ReplayStorageTypeChoices.s3.value: ReplayStorageTypeS3Serializer, - const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer, - const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer, - const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer, - const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer, - const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer, - const.ReplayStorageTypeChoices.cos.value: ReplayStorageTypeCOSSerializer + const.ReplayStorageType.s3.value: ReplayStorageTypeS3Serializer, + const.ReplayStorageType.ceph.value: ReplayStorageTypeCephSerializer, + const.ReplayStorageType.swift.value: ReplayStorageTypeSwiftSerializer, + const.ReplayStorageType.oss.value: ReplayStorageTypeOSSSerializer, + const.ReplayStorageType.azure.value: ReplayStorageTypeAzureSerializer, + const.ReplayStorageType.obs.value: ReplayStorageTypeOBSSerializer, + const.ReplayStorageType.cos.value: ReplayStorageTypeCOSSerializer } @@ -172,7 +172,7 @@ class CommandStorageTypeESSerializer(serializers.Serializer): # mapping command_storage_type_serializer_classes_mapping = { - const.CommandStorageTypeChoices.es.value: CommandStorageTypeESSerializer + const.CommandStorageType.es.value: CommandStorageTypeESSerializer } diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 1ef4f6158..4b2e3614e 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -2,28 +2,26 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from common.drf.serializers import BulkModelSerializer -from common.utils import is_uuid +from common.drf.fields import LabeledChoiceField +from common.utils import get_request_ip, pretty_string, is_uuid from users.serializers import ServiceAccountSerializer -from common.utils import get_request_ip, pretty_string from .. import const - -from ..models import ( - Terminal, Status, Task, CommandStorage, ReplayStorage -) +from ..models import Terminal, Status, Task, CommandStorage, ReplayStorage -class StatusSerializer(serializers.ModelSerializer): +class StatSerializer(serializers.ModelSerializer): sessions = serializers.ListSerializer( - child=serializers.CharField(max_length=36), write_only=True + child=serializers.CharField(max_length=36), + write_only=True ) class Meta: + model = Status fields_mini = ['id'] fields_write_only = ['sessions', ] fields_small = fields_mini + fields_write_only + [ 'cpu_load', 'memory_used', 'disk_used', - 'session_online', - 'date_created' + 'session_online', 'date_created' ] fields_fk = ['terminal'] fields = fields_small + fields_fk @@ -32,30 +30,28 @@ class StatusSerializer(serializers.ModelSerializer): "memory_used": {'default': 0}, "disk_used": {'default': 0}, } - model = Status class TerminalSerializer(BulkModelSerializer): session_online = serializers.ReadOnlyField(source='get_online_session_count') is_alive = serializers.BooleanField(read_only=True) is_active = serializers.BooleanField(read_only=True, label='Is active') - status = serializers.ChoiceField( - read_only=True, choices=const.ComponentStatusChoices.choices, - source='latest_status', label=_('Load status') + load = LabeledChoiceField( + read_only=True, choices=const.ComponentLoad.choices, + label=_('Load status') ) - status_display = serializers.CharField(read_only=True, source='latest_status_display') - stat = StatusSerializer(read_only=True, source='latest_stat') + stat = StatSerializer(read_only=True, source='last_stat') class Meta: model = Terminal fields_mini = ['id', 'name'] fields_small = fields_mini + [ - 'type', 'remote_addr', 'http_port', 'ssh_port', - 'session_online', 'command_storage', 'replay_storage', - 'is_accepted', "is_active", 'is_alive', + 'type', 'remote_addr', 'session_online', + 'command_storage', 'replay_storage', + 'is_active', 'is_alive', 'date_created', 'comment', ] - fields_fk = ['status', 'status_display', 'stat'] + fields_fk = ['load', 'stat'] fields = fields_small + fields_fk read_only_fields = ['type', 'date_created'] extra_kwargs = { diff --git a/apps/terminal/signal_handlers.py b/apps/terminal/signal_handlers.py index bac9c3253..cbbed376b 100644 --- a/apps/terminal/signal_handlers.py +++ b/apps/terminal/signal_handlers.py @@ -4,7 +4,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver - from .models import Applet, AppletHost @@ -14,6 +13,7 @@ def on_applet_host_create(sender, instance, created=False, **kwargs): return applets = Applet.objects.all() instance.applets.set(applets) + instance.generate_accounts() @receiver(post_save, sender=Applet) diff --git a/apps/terminal/startup.py b/apps/terminal/startup.py index 672d31830..1b4d7a7e8 100644 --- a/apps/terminal/startup.py +++ b/apps/terminal/startup.py @@ -9,8 +9,8 @@ from common.db.utils import close_old_connections from common.decorator import Singleton from common.utils import get_disk_usage, get_cpu_load, get_memory_usage, get_logger -from .serializers.terminal import TerminalRegistrationSerializer, StatusSerializer -from .const import TerminalTypeChoices +from .serializers.terminal import TerminalRegistrationSerializer, StatSerializer +from .const import TerminalType from .models import Terminal __all__ = ['CoreTerminal', 'CeleryTerminal'] @@ -51,16 +51,18 @@ class BaseTerminal(object): 'disk_used': get_disk_usage(path=settings.BASE_DIR), 'sessions': [], } - status_serializer = StatusSerializer(data=heartbeat_data) + status_serializer = StatSerializer(data=heartbeat_data) status_serializer.is_valid() status_serializer.validated_data.pop('sessions', None) terminal = self.get_or_register_terminal() status_serializer.validated_data['terminal'] = terminal try: - status_serializer.save() + status = status_serializer.save() + print("Save status ok: ", status) time.sleep(self.interval) except OperationalError: + print("Save status error, close old connections") close_old_connections() def get_or_register_terminal(self): @@ -90,8 +92,8 @@ class CoreTerminal(BaseTerminal): def __init__(self): super().__init__( - suffix_name=TerminalTypeChoices.core.label, - _type=TerminalTypeChoices.core.value + suffix_name=TerminalType.core.label, + _type=TerminalType.core.value ) @@ -99,6 +101,6 @@ class CoreTerminal(BaseTerminal): class CeleryTerminal(BaseTerminal): def __init__(self): super().__init__( - suffix_name=TerminalTypeChoices.celery.label, - _type=TerminalTypeChoices.celery.value + suffix_name=TerminalType.celery.label, + _type=TerminalType.celery.value ) diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index eed67f408..53ef629f2 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -1,17 +1,19 @@ # -*- coding: utf-8 -*- # import os +import time from itertools import groupby, chain +from collections import defaultdict +from django.utils import timezone from django.conf import settings from django.core.files.storage import default_storage - import jms_storage from common.utils import get_logger +from tickets.models import TicketSession from . import const from .models import ReplayStorage -from tickets.models import TicketSession logger = get_logger(__name__) @@ -76,16 +78,16 @@ def get_session_replay_url(session): return local_path, url -class ComputeStatUtil: +class ComputeLoadUtil: # system status @staticmethod def _common_compute_system_status(value, thresholds): if thresholds[0] <= value <= thresholds[1]: - return const.ComponentStatusChoices.normal.value + return const.ComponentLoad.normal.value elif thresholds[1] < value <= thresholds[2]: - return const.ComponentStatusChoices.high.value + return const.ComponentLoad.high.value else: - return const.ComponentStatusChoices.critical.value + return const.ComponentLoad.critical.value @classmethod def _compute_system_stat_status(cls, stat): @@ -106,16 +108,16 @@ class ComputeStatUtil: return system_status @classmethod - def compute_component_status(cls, stat): - if not stat: - return const.ComponentStatusChoices.offline + def compute_load(cls, stat): + if not stat or time.time() - stat.date_created.timestamp() > 150: + return const.ComponentLoad.offline system_status_values = cls._compute_system_stat_status(stat).values() - if const.ComponentStatusChoices.critical in system_status_values: - return const.ComponentStatusChoices.critical - elif const.ComponentStatusChoices.high in system_status_values: - return const.ComponentStatusChoices.high + if const.ComponentLoad.critical in system_status_values: + return const.ComponentLoad.critical + elif const.ComponentLoad.high in system_status_values: + return const.ComponentLoad.high else: - return const.ComponentStatusChoices.normal + return const.ComponentLoad.normal class TypedComponentsStatusMetricsUtil(object): @@ -135,31 +137,15 @@ class TypedComponentsStatusMetricsUtil(object): def get_metrics(self): metrics = [] for _tp, components in self.grouped_components: - normal_count = high_count = critical_count = 0 - total_count = offline_count = session_online_total = 0 - + metric = { + 'normal': 0, 'high': 0, 'critical': 0, 'offline': 0, + 'total': 0, 'session_active': 0, 'type': _tp + } for component in components: - total_count += 1 - if not component.is_alive: - offline_count += 1 - continue - if component.is_normal: - normal_count += 1 - elif component.is_high: - high_count += 1 - else: - # critical - critical_count += 1 - session_online_total += component.get_online_session_count() - metrics.append({ - 'total': total_count, - 'normal': normal_count, - 'high': high_count, - 'critical': critical_count, - 'offline': offline_count, - 'session_active': session_online_total, - 'type': _tp, - }) + metric[component.load] += 1 + metric['total'] += 1 + metric['session_active'] += component.get_online_session_count() + metrics.append(metric) return metrics