diff --git a/.dockerignore b/.dockerignore
index a504fb4ec..81c9033ba 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -8,4 +8,4 @@ celerybeat.pid
### Vagrant ###
.vagrant/
apps/xpack/.git
-
+.history/
diff --git a/.github/ISSUE_TEMPLATE/----.md b/.github/ISSUE_TEMPLATE/----.md
index b407e8a25..147f42db4 100644
--- a/.github/ISSUE_TEMPLATE/----.md
+++ b/.github/ISSUE_TEMPLATE/----.md
@@ -3,7 +3,10 @@ name: 需求建议
about: 提出针对本项目的想法和建议
title: "[Feature] "
labels: 类型:需求
-assignees: ibuler
+assignees:
+ - ibuler
+ - baijiangjie
+
---
diff --git a/.github/ISSUE_TEMPLATE/bug---.md b/.github/ISSUE_TEMPLATE/bug---.md
index 3c590459c..e4a21adde 100644
--- a/.github/ISSUE_TEMPLATE/bug---.md
+++ b/.github/ISSUE_TEMPLATE/bug---.md
@@ -3,7 +3,9 @@ name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] "
labels: 类型:bug
-assignees: wojiushixiaobai
+assignees:
+ - wojiushixiaobai
+ - baijiangjie
---
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index 1dd2a68d6..b15719590 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -3,7 +3,9 @@ name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] "
labels: 类型:提问
-assignees: wojiushixiaobai
+assignees:
+ - wojiushixiaobai
+ - baijiangjie
---
diff --git a/.gitignore b/.gitignore
index 316630d9f..9573a70b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,4 @@ releashe
/apps/script.py
data/*
test.py
+.history/
diff --git a/Dockerfile b/Dockerfile
index a7d6d2b9c..4cad1e0bd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -55,7 +55,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
- && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
+ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
diff --git a/Dockerfile.loong64 b/Dockerfile.loong64
index 2d44b00df..629623a85 100644
--- a/Dockerfile.loong64
+++ b/Dockerfile.loong64
@@ -53,7 +53,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
- && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
+ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
diff --git a/README.md b/README.md
index c5a1d2d2a..ca31cce08 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org)
+- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
## 案例研究
diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py
index d12f11549..7b9991988 100644
--- a/apps/accounts/api/account/account.py
+++ b/apps/accounts/api/account/account.py
@@ -1,20 +1,21 @@
from django.shortcuts import get_object_or_404
from rest_framework.decorators import action
-from rest_framework.generics import ListAPIView
+from rest_framework.generics import ListAPIView, CreateAPIView
from rest_framework.response import Response
+from rest_framework.status import HTTP_200_OK
from accounts import serializers
from accounts.filters import AccountFilterSet
from accounts.models import Account
from assets.models import Asset, Node
-from common.permissions import UserConfirmation, ConfirmType
+from common.permissions import UserConfirmation, ConfirmType, IsValidUser
from common.views.mixins import RecordViewLogMixin
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
__all__ = [
'AccountViewSet', 'AccountSecretsViewSet',
- 'AccountHistoriesSecretAPI'
+ 'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
]
@@ -28,7 +29,7 @@ class AccountViewSet(OrgBulkModelViewSet):
rbac_perms = {
'partial_update': ['accounts.change_account'],
'su_from_accounts': 'accounts.view_account',
- 'username_suggestions': 'accounts.view_account',
+ 'clear_secret': 'accounts.change_account',
}
@action(methods=['get'], detail=False, url_path='su-from-accounts')
@@ -48,7 +49,10 @@ class AccountViewSet(OrgBulkModelViewSet):
serializer = serializers.AccountSerializer(accounts, many=True)
return Response(data=serializer.data)
- @action(methods=['get'], detail=False, url_path='username-suggestions')
+ @action(
+ methods=['get'], detail=False, url_path='username-suggestions',
+ permission_classes=[IsValidUser]
+ )
def username_suggestions(self, request, *args, **kwargs):
asset_ids = request.query_params.get('assets')
node_keys = request.query_params.get('keys')
@@ -71,6 +75,12 @@ class AccountViewSet(OrgBulkModelViewSet):
usernames = common + others
return Response(data=usernames)
+ @action(methods=['patch'], detail=False, url_path='clear-secret')
+ def clear_secret(self, request, *args, **kwargs):
+ account_ids = request.data.get('account_ids', [])
+ self.model.objects.filter(id__in=account_ids).update(secret=None)
+ return Response(status=HTTP_200_OK)
+
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
"""
@@ -87,6 +97,20 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
}
+class AssetAccountBulkCreateApi(CreateAPIView):
+ serializer_class = serializers.AssetAccountBulkSerializer
+ rbac_perms = {
+ 'POST': 'accounts.add_account',
+ }
+
+ def create(self, request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ data = serializer.create(serializer.validated_data)
+ serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True)
+ return Response(data=serializer.data, status=HTTP_200_OK)
+
+
class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
model = Account.history.model
serializer_class = serializers.AccountHistorySerializer
diff --git a/apps/accounts/api/account/task.py b/apps/accounts/api/account/task.py
index a042323f5..2f3f11dae 100644
--- a/apps/accounts/api/account/task.py
+++ b/apps/accounts/api/account/task.py
@@ -31,8 +31,8 @@ class AccountsTaskCreateAPI(CreateAPIView):
else:
account = accounts[0]
asset = account.asset
- if not asset.auto_info['ansible_enabled'] or \
- not asset.auto_info['ping_enabled']:
+ if not asset.auto_config['ansible_enabled'] or \
+ not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError()
task = verify_accounts_connectivity_task.delay(account_ids)
diff --git a/apps/accounts/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py
index 3abca94dd..e6a846368 100644
--- a/apps/accounts/api/automations/gather_accounts.py
+++ b/apps/accounts/api/automations/gather_accounts.py
@@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
#
-from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from accounts import serializers
from accounts.const import AutomationTypes
-from accounts.const import Source
from accounts.filters import GatheredAccountFilterSet
from accounts.models import GatherAccountsAutomation
from accounts.models import GatheredAccount
@@ -50,22 +48,12 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
'default': serializers.GatheredAccountSerializer,
}
rbac_perms = {
- 'sync_account': 'assets.add_gatheredaccount',
+ 'sync_accounts': 'assets.add_gatheredaccount',
}
- @action(methods=['post'], detail=True, url_path='sync')
- def sync_account(self, request, *args, **kwargs):
- gathered_account = super().get_object()
- asset = gathered_account.asset
- username = gathered_account.username
- accounts = asset.accounts.filter(username=username)
-
- if accounts.exists():
- accounts.update(source=Source.COLLECTED)
- else:
- asset.accounts.model.objects.create(
- asset=asset, username=username,
- name=f'{username}-{_("Collected")}',
- source=Source.COLLECTED
- )
+ @action(methods=['post'], detail=False, url_path='sync-accounts')
+ def sync_accounts(self, request, *args, **kwargs):
+ gathered_account_ids = request.data.get('gathered_account_ids')
+ gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids)
+ self.model.sync_accounts(gathered_accounts)
return Response(status=status.HTTP_201_CREATED)
diff --git a/apps/accounts/automations/change_secret/host/aix/main.yml b/apps/accounts/automations/change_secret/host/aix/main.yml
index 3e3daae7f..4bb571f62 100644
--- a/apps/accounts/automations/change_secret/host/aix/main.yml
+++ b/apps/accounts/automations/change_secret/host/aix/main.yml
@@ -18,18 +18,18 @@
- name: remove jumpserver ssh key
ansible.builtin.lineinfile:
- dest: "{{ kwargs.dest }}"
- regexp: "{{ kwargs.regexp }}"
+ dest: "{{ ssh_params.dest }}"
+ regexp: "{{ ssh_params.regexp }}"
state: absent
when:
- account.secret_type == "ssh_key"
- - kwargs.strategy == "set_jms"
+ - ssh_params.strategy == "set_jms"
- name: Change SSH key
ansible.builtin.authorized_key:
user: "{{ account.username }}"
key: "{{ account.secret }}"
- exclusive: "{{ kwargs.exclusive }}"
+ exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key"
- name: Refresh connection
diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml
index 932f3cade..8dea25c12 100644
--- a/apps/accounts/automations/change_secret/host/posix/main.yml
+++ b/apps/accounts/automations/change_secret/host/posix/main.yml
@@ -18,18 +18,18 @@
- name: remove jumpserver ssh key
ansible.builtin.lineinfile:
- dest: "{{ kwargs.dest }}"
- regexp: "{{ kwargs.regexp }}"
+ dest: "{{ ssh_params.dest }}"
+ regexp: "{{ ssh_params.regexp }}"
state: absent
when:
- account.secret_type == "ssh_key"
- - kwargs.strategy == "set_jms"
+ - ssh_params.strategy == "set_jms"
- name: Change SSH key
ansible.builtin.authorized_key:
user: "{{ account.username }}"
key: "{{ account.secret }}"
- exclusive: "{{ kwargs.exclusive }}"
+ exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key"
- name: Refresh connection
diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py
index 41bad5bda..05e2b1349 100644
--- a/apps/accounts/automations/change_secret/manager.py
+++ b/apps/accounts/automations/change_secret/manager.py
@@ -42,7 +42,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def method_type(cls):
return AutomationTypes.change_secret
- def get_kwargs(self, account, secret, secret_type):
+ def get_ssh_params(self, account, secret, secret_type):
kwargs = {}
if secret_type != SecretType.SSH_KEY:
return kwargs
@@ -76,6 +76,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
accounts = accounts.filter(id__in=self.account_ids)
if self.secret_type:
accounts = accounts.filter(secret_type=self.secret_type)
+
+ if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
+ accounts = accounts.filter(privileged=False).exclude(
+ username__in=['root', 'administrator']
+ )
return accounts
def host_callback(
@@ -106,6 +111,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
print(f'Windows {asset} does not support ssh key push')
return inventory_hosts
+ host['ssh_params'] = {}
for account in accounts:
h = deepcopy(host)
secret_type = account.secret_type
@@ -124,7 +130,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret)
- h['kwargs'] = self.get_kwargs(account, new_secret, secret_type)
+ h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
h['account'] = {
'name': account.name,
'username': account.username,
diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py
index 2ecd3d2e1..c4ba6b5a0 100644
--- a/apps/accounts/automations/gather_accounts/manager.py
+++ b/apps/accounts/automations/gather_accounts/manager.py
@@ -12,6 +12,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.host_asset_mapper = {}
+ self.is_sync_account = self.execution.snapshot.get('is_sync_account')
@classmethod
def method_type(cls):
@@ -25,26 +26,38 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def filter_success_result(self, tp, result):
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
return result
-
@staticmethod
- def update_or_create_gathered_accounts(asset, result):
+ def generate_data(asset, result):
+ data = []
+ for username, info in result.items():
+ d = {'asset': asset, 'username': username, 'present': True}
+ if info.get('date'):
+ d['date_last_login'] = info['date']
+ if info.get('address'):
+ d['address_last_login'] = info['address'][:32]
+ data.append(d)
+ return data
+
+ def update_or_create_accounts(self, asset, result):
+ data = self.generate_data(asset, result)
with tmp_to_org(asset.org_id):
+ gathered_accounts = []
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
- for username, data in result.items():
- d = {'asset': asset, 'username': username, 'present': True}
- if data.get('date'):
- d['date_last_login'] = data['date']
- if data.get('address'):
- d['address_last_login'] = data['address'][:32]
- GatheredAccount.objects.update_or_create(
+ for d in data:
+ username = d['username']
+ gathered_account, __ = GatheredAccount.objects.update_or_create(
defaults=d, asset=asset, username=username,
)
+ gathered_accounts.append(gathered_account)
+ if not self.is_sync_account:
+ return
+ GatheredAccount.sync_accounts(gathered_accounts)
def on_host_success(self, host, result):
info = result.get('debug', {}).get('res', {}).get('info', {})
asset = self.host_asset_mapper.get(host)
if asset and info:
result = self.filter_success_result(asset.type, info)
- self.update_or_create_gathered_accounts(asset, result)
+ self.update_or_create_accounts(asset, result)
else:
logger.error("Not found info".format(host))
diff --git a/apps/accounts/automations/methods.py b/apps/accounts/automations/methods.py
index be5890701..557202184 100644
--- a/apps/accounts/automations/methods.py
+++ b/apps/accounts/automations/methods.py
@@ -1,30 +1,6 @@
import os
-import copy
-from accounts.const import AutomationTypes
from assets.automations.methods import get_platform_automation_methods
-
-def copy_change_secret_to_push_account(methods):
- push_account = AutomationTypes.push_account
- change_secret = AutomationTypes.change_secret
- copy_methods = copy.deepcopy(methods)
- for method in copy_methods:
- if not method['id'].startswith(change_secret):
- continue
- copy_method = copy.deepcopy(method)
- copy_method['method'] = push_account.value
- copy_method['id'] = copy_method['id'].replace(
- change_secret, push_account
- )
- copy_method['name'] = copy_method['name'].replace(
- 'Change secret', 'Push account'
- )
- methods.append(copy_method)
- return methods
-
-
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-automation_methods = get_platform_automation_methods(BASE_DIR)
-
-platform_automation_methods = copy_change_secret_to_push_account(automation_methods)
+platform_automation_methods = get_platform_automation_methods(BASE_DIR)
diff --git a/apps/accounts/automations/push_account/database/mongodb/main.yml b/apps/accounts/automations/push_account/database/mongodb/main.yml
new file mode 100644
index 000000000..42ccd78ea
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/mongodb/main.yml
@@ -0,0 +1,58 @@
+- hosts: mongodb
+ gather_facts: no
+ vars:
+ ansible_python_interpreter: /usr/local/bin/python
+
+ tasks:
+ - name: Test MongoDB connection
+ mongodb_ping:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_database: "{{ jms_asset.spec_info.db_name }}"
+ ssl: "{{ jms_asset.spec_info.use_ssl }}"
+ ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
+ ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
+ connection_options:
+ - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
+ register: db_info
+
+ - name: Display MongoDB version
+ debug:
+ var: db_info.server_version
+ when: db_info is succeeded
+
+ - name: Change MongoDB password
+ mongodb_user:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_database: "{{ jms_asset.spec_info.db_name }}"
+ ssl: "{{ jms_asset.spec_info.use_ssl }}"
+ ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
+ ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
+ connection_options:
+ - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
+ db: "{{ jms_asset.spec_info.db_name }}"
+ name: "{{ account.username }}"
+ password: "{{ account.secret }}"
+ when: db_info is succeeded
+ register: change_info
+
+ - name: Verify password
+ mongodb_ping:
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_database: "{{ jms_asset.spec_info.db_name }}"
+ ssl: "{{ jms_asset.spec_info.use_ssl }}"
+ ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
+ ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
+ connection_options:
+ - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
+ when:
+ - db_info is succeeded
+ - change_info is succeeded
diff --git a/apps/accounts/automations/push_account/database/mongodb/manifest.yml b/apps/accounts/automations/push_account/database/mongodb/manifest.yml
new file mode 100644
index 000000000..6c91977bf
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/mongodb/manifest.yml
@@ -0,0 +1,6 @@
+id: push_account_mongodb
+name: Push account for MongoDB
+category: database
+type:
+ - mongodb
+method: push_account
diff --git a/apps/accounts/automations/push_account/database/mysql/main.yml b/apps/accounts/automations/push_account/database/mysql/main.yml
new file mode 100644
index 000000000..26858c94e
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/mysql/main.yml
@@ -0,0 +1,43 @@
+- hosts: mysql
+ gather_facts: no
+ vars:
+ ansible_python_interpreter: /usr/local/bin/python
+ db_name: "{{ jms_asset.spec_info.db_name }}"
+
+ tasks:
+ - name: Test MySQL connection
+ community.mysql.mysql_info:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ filter: version
+ register: db_info
+
+ - name: MySQL version
+ debug:
+ var: db_info.version.full
+
+ - name: Change MySQL password
+ community.mysql.mysql_user:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ name: "{{ account.username }}"
+ password: "{{ account.secret }}"
+ host: "%"
+ priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
+ when: db_info is succeeded
+ register: change_info
+
+ - name: Verify password
+ community.mysql.mysql_info:
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ filter: version
+ when:
+ - db_info is succeeded
+ - change_info is succeeded
\ No newline at end of file
diff --git a/apps/accounts/automations/push_account/database/mysql/manifest.yml b/apps/accounts/automations/push_account/database/mysql/manifest.yml
new file mode 100644
index 000000000..712d0bfb8
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/mysql/manifest.yml
@@ -0,0 +1,7 @@
+id: push_account_mysql
+name: Push account for MySQL
+category: database
+type:
+ - mysql
+ - mariadb
+method: push_account
diff --git a/apps/accounts/automations/push_account/database/oracle/main.yml b/apps/accounts/automations/push_account/database/oracle/main.yml
new file mode 100644
index 000000000..ad58e0584
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/oracle/main.yml
@@ -0,0 +1,44 @@
+- hosts: oracle
+ gather_facts: no
+ vars:
+ ansible_python_interpreter: /usr/local/bin/python
+
+ tasks:
+ - name: Test Oracle connection
+ oracle_ping:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_database: "{{ jms_asset.spec_info.db_name }}"
+ mode: "{{ jms_account.mode }}"
+ register: db_info
+
+ - name: Display Oracle version
+ debug:
+ var: db_info.server_version
+ when: db_info is succeeded
+
+ - name: Change Oracle password
+ oracle_user:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_database: "{{ jms_asset.spec_info.db_name }}"
+ mode: "{{ jms_account.mode }}"
+ name: "{{ account.username }}"
+ password: "{{ account.secret }}"
+ when: db_info is succeeded
+ register: change_info
+
+ - name: Verify password
+ oracle_ping:
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_database: "{{ jms_asset.spec_info.db_name }}"
+ when:
+ - db_info is succeeded
+ - change_info is succeeded
diff --git a/apps/accounts/automations/push_account/database/oracle/manifest.yml b/apps/accounts/automations/push_account/database/oracle/manifest.yml
new file mode 100644
index 000000000..f60215892
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/oracle/manifest.yml
@@ -0,0 +1,6 @@
+id: push_account_oracle
+name: Push account for Oracle
+category: database
+type:
+ - oracle
+method: push_account
diff --git a/apps/accounts/automations/push_account/database/postgresql/main.yml b/apps/accounts/automations/push_account/database/postgresql/main.yml
new file mode 100644
index 000000000..dbb11af12
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/postgresql/main.yml
@@ -0,0 +1,46 @@
+- hosts: postgre
+ gather_facts: no
+ vars:
+ ansible_python_interpreter: /usr/local/bin/python
+
+ tasks:
+ - name: Test PostgreSQL connection
+ community.postgresql.postgresql_ping:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ login_db: "{{ jms_asset.spec_info.db_name }}"
+ register: result
+ failed_when: not result.is_available
+
+ - name: Display PostgreSQL version
+ debug:
+ var: result.server_version.full
+ when: result is succeeded
+
+ - name: Change PostgreSQL password
+ community.postgresql.postgresql_user:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ db: "{{ jms_asset.spec_info.db_name }}"
+ name: "{{ account.username }}"
+ password: "{{ account.secret }}"
+ role_attr_flags: LOGIN
+ when: result is succeeded
+ register: change_info
+
+ - name: Verify password
+ community.postgresql.postgresql_ping:
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ db: "{{ jms_asset.spec_info.db_name }}"
+ when:
+ - result is succeeded
+ - change_info is succeeded
+ register: result
+ failed_when: not result.is_available
diff --git a/apps/accounts/automations/push_account/database/postgresql/manifest.yml b/apps/accounts/automations/push_account/database/postgresql/manifest.yml
new file mode 100644
index 000000000..6488ddd5a
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/postgresql/manifest.yml
@@ -0,0 +1,6 @@
+id: push_account_postgresql
+name: Push account for PostgreSQL
+category: database
+type:
+ - postgresql
+method: push_account
diff --git a/apps/accounts/automations/push_account/database/sqlserver/main.yml b/apps/accounts/automations/push_account/database/sqlserver/main.yml
new file mode 100644
index 000000000..da0427f5c
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/sqlserver/main.yml
@@ -0,0 +1,69 @@
+- hosts: sqlserver
+ gather_facts: no
+ vars:
+ ansible_python_interpreter: /usr/local/bin/python
+
+ tasks:
+ - name: Test SQLServer connection
+ community.general.mssql_script:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ name: '{{ jms_asset.spec_info.db_name }}'
+ script: |
+ SELECT @@version
+ register: db_info
+
+ - name: SQLServer version
+ set_fact:
+ info:
+ version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}"
+ - debug:
+ var: info
+
+ - name: Check whether SQLServer User exist
+ community.general.mssql_script:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ name: '{{ jms_asset.spec_info.db_name }}'
+ script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
+ when: db_info is succeeded
+ register: user_exist
+
+ - name: Change SQLServer password
+ community.general.mssql_script:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ name: '{{ jms_asset.spec_info.db_name }}'
+ script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
+ when: user_exist.query_results[0] | length != 0
+ register: change_info
+
+ - name: Add SQLServer user
+ community.general.mssql_script:
+ login_user: "{{ jms_account.username }}"
+ login_password: "{{ jms_account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ name: '{{ jms_asset.spec_info.db_name }}'
+ script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
+ when: user_exist.query_results[0] | length == 0
+ register: change_info
+
+ - name: Verify password
+ community.general.mssql_script:
+ login_user: "{{ account.username }}"
+ login_password: "{{ account.secret }}"
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
+ name: '{{ jms_asset.spec_info.db_name }}'
+ script: |
+ SELECT @@version
+ when:
+ - db_info is succeeded
+ - change_info is succeeded
diff --git a/apps/accounts/automations/push_account/database/sqlserver/manifest.yml b/apps/accounts/automations/push_account/database/sqlserver/manifest.yml
new file mode 100644
index 000000000..f1dc32b66
--- /dev/null
+++ b/apps/accounts/automations/push_account/database/sqlserver/manifest.yml
@@ -0,0 +1,6 @@
+id: push_account_sqlserver
+name: Push account for SQLServer
+category: database
+type:
+ - sqlserver
+method: push_account
diff --git a/apps/accounts/automations/push_account/host/aix/main.yml b/apps/accounts/automations/push_account/host/aix/main.yml
new file mode 100644
index 000000000..9ac68d20e
--- /dev/null
+++ b/apps/accounts/automations/push_account/host/aix/main.yml
@@ -0,0 +1,93 @@
+- hosts: demo
+ gather_facts: no
+ tasks:
+ - name: Test privileged account
+ ansible.builtin.ping:
+
+ - name: Push user
+ ansible.builtin.user:
+ name: "{{ account.username }}"
+ shell: "{{ params.shell }}"
+ home: "{{ '/home/' + account.username }}"
+ groups: "{{ params.groups }}"
+ expires: -1
+ state: present
+
+ - name: "Add {{ account.username }} group"
+ ansible.builtin.group:
+ name: "{{ account.username }}"
+ state: present
+
+ - name: Check home dir exists
+ ansible.builtin.stat:
+ path: "{{ '/home/' + account.username }}"
+ register: home_existed
+
+ - name: Set home dir permission
+ ansible.builtin.file:
+ path: "{{ '/home/' + account.username }}"
+ owner: "{{ account.username }}"
+ group: "{{ account.username }}"
+ mode: "0700"
+ when:
+ - home_existed.stat.exists == true
+
+ - name: Add user groups
+ ansible.builtin.user:
+ name: "{{ account.username }}"
+ groups: "{{ params.groups }}"
+ when: params.groups
+
+ - name: Push user password
+ ansible.builtin.user:
+ name: "{{ account.username }}"
+ password: "{{ account.secret | password_hash('sha512') }}"
+ update_password: always
+ when: account.secret_type == "password"
+
+ - name: remove jumpserver ssh key
+ ansible.builtin.lineinfile:
+ dest: "{{ ssh_params.dest }}"
+ regexp: "{{ ssh_params.regexp }}"
+ state: absent
+ when:
+ - account.secret_type == "ssh_key"
+ - ssh_params.strategy == "set_jms"
+
+ - name: Push SSH key
+ ansible.builtin.authorized_key:
+ user: "{{ account.username }}"
+ key: "{{ account.secret }}"
+ exclusive: "{{ ssh_params.exclusive }}"
+ when: account.secret_type == "ssh_key"
+
+ - name: Set sudo setting
+ ansible.builtin.lineinfile:
+ dest: /etc/sudoers
+ state: present
+ regexp: "^{{ account.username }} ALL="
+ line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
+ validate: visudo -cf %s
+ when:
+ - params.sudo
+
+ - name: Refresh connection
+ ansible.builtin.meta: reset_connection
+
+ - name: Verify password
+ ansible.builtin.ping:
+ become: no
+ vars:
+ ansible_user: "{{ account.username }}"
+ ansible_password: "{{ account.secret }}"
+ ansible_become: no
+ when: account.secret_type == "password"
+
+ - name: Verify SSH key
+ ansible.builtin.ping:
+ become: no
+ vars:
+ ansible_user: "{{ account.username }}"
+ ansible_ssh_private_key_file: "{{ account.private_key_path }}"
+ ansible_become: no
+ when: account.secret_type == "ssh_key"
diff --git a/apps/accounts/automations/push_account/host/aix/manifest.yml b/apps/accounts/automations/push_account/host/aix/manifest.yml
new file mode 100644
index 000000000..ccc051eac
--- /dev/null
+++ b/apps/accounts/automations/push_account/host/aix/manifest.yml
@@ -0,0 +1,24 @@
+id: push_account_aix
+name: Push account for aix
+category: host
+type:
+ - AIX
+method: push_account
+params:
+ - name: sudo
+ type: str
+ label: 'Sudo'
+ default: '/bin/whoami'
+ help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
+
+ - name: shell
+ type: str
+ label: 'Shell'
+ default: '/bin/bash'
+
+ - name: groups
+ type: str
+ label: '用户组'
+ default: ''
+ help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
+
diff --git a/apps/accounts/automations/push_account/host/posix/main.yml b/apps/accounts/automations/push_account/host/posix/main.yml
new file mode 100644
index 000000000..9ac68d20e
--- /dev/null
+++ b/apps/accounts/automations/push_account/host/posix/main.yml
@@ -0,0 +1,93 @@
+- hosts: demo
+ gather_facts: no
+ tasks:
+ - name: Test privileged account
+ ansible.builtin.ping:
+
+ - name: Push user
+ ansible.builtin.user:
+ name: "{{ account.username }}"
+ shell: "{{ params.shell }}"
+ home: "{{ '/home/' + account.username }}"
+ groups: "{{ params.groups }}"
+ expires: -1
+ state: present
+
+ - name: "Add {{ account.username }} group"
+ ansible.builtin.group:
+ name: "{{ account.username }}"
+ state: present
+
+ - name: Check home dir exists
+ ansible.builtin.stat:
+ path: "{{ '/home/' + account.username }}"
+ register: home_existed
+
+ - name: Set home dir permission
+ ansible.builtin.file:
+ path: "{{ '/home/' + account.username }}"
+ owner: "{{ account.username }}"
+ group: "{{ account.username }}"
+ mode: "0700"
+ when:
+ - home_existed.stat.exists == true
+
+ - name: Add user groups
+ ansible.builtin.user:
+ name: "{{ account.username }}"
+ groups: "{{ params.groups }}"
+ when: params.groups
+
+ - name: Push user password
+ ansible.builtin.user:
+ name: "{{ account.username }}"
+ password: "{{ account.secret | password_hash('sha512') }}"
+ update_password: always
+ when: account.secret_type == "password"
+
+ - name: remove jumpserver ssh key
+ ansible.builtin.lineinfile:
+ dest: "{{ ssh_params.dest }}"
+ regexp: "{{ ssh_params.regexp }}"
+ state: absent
+ when:
+ - account.secret_type == "ssh_key"
+ - ssh_params.strategy == "set_jms"
+
+ - name: Push SSH key
+ ansible.builtin.authorized_key:
+ user: "{{ account.username }}"
+ key: "{{ account.secret }}"
+ exclusive: "{{ ssh_params.exclusive }}"
+ when: account.secret_type == "ssh_key"
+
+ - name: Set sudo setting
+ ansible.builtin.lineinfile:
+ dest: /etc/sudoers
+ state: present
+ regexp: "^{{ account.username }} ALL="
+ line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
+ validate: visudo -cf %s
+ when:
+ - params.sudo
+
+ - name: Refresh connection
+ ansible.builtin.meta: reset_connection
+
+ - name: Verify password
+ ansible.builtin.ping:
+ become: no
+ vars:
+ ansible_user: "{{ account.username }}"
+ ansible_password: "{{ account.secret }}"
+ ansible_become: no
+ when: account.secret_type == "password"
+
+ - name: Verify SSH key
+ ansible.builtin.ping:
+ become: no
+ vars:
+ ansible_user: "{{ account.username }}"
+ ansible_ssh_private_key_file: "{{ account.private_key_path }}"
+ ansible_become: no
+ when: account.secret_type == "ssh_key"
diff --git a/apps/accounts/automations/push_account/host/posix/manifest.yml b/apps/accounts/automations/push_account/host/posix/manifest.yml
new file mode 100644
index 000000000..382b48add
--- /dev/null
+++ b/apps/accounts/automations/push_account/host/posix/manifest.yml
@@ -0,0 +1,25 @@
+id: push_account_posix
+name: Push account for posix
+category: host
+type:
+ - unix
+ - linux
+method: push_account
+params:
+ - name: sudo
+ type: str
+ label: 'Sudo'
+ default: '/bin/whoami'
+ help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
+
+ - name: shell
+ type: str
+ label: 'Shell'
+ default: '/bin/bash'
+ help_text: ''
+
+ - name: groups
+ type: str
+ label: '用户组'
+ default: ''
+ help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
diff --git a/apps/accounts/automations/push_account/host/windows/main.yml b/apps/accounts/automations/push_account/host/windows/main.yml
new file mode 100644
index 000000000..8a2a0aef0
--- /dev/null
+++ b/apps/accounts/automations/push_account/host/windows/main.yml
@@ -0,0 +1,30 @@
+- hosts: demo
+ gather_facts: no
+ tasks:
+ - name: Test privileged account
+ ansible.windows.win_ping:
+
+# - name: Print variables
+# debug:
+# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
+
+ - name: Push user password
+ ansible.windows.win_user:
+ fullname: "{{ account.username}}"
+ name: "{{ account.username }}"
+ password: "{{ account.secret }}"
+ password_never_expires: yes
+ groups: "{{ params.groups }}"
+ groups_action: add
+ update_password: always
+ when: account.secret_type == "password"
+
+ - name: Refresh connection
+ ansible.builtin.meta: reset_connection
+
+ - name: Verify password
+ ansible.windows.win_ping:
+ vars:
+ ansible_user: "{{ account.username }}"
+ ansible_password: "{{ account.secret }}"
+ when: account.secret_type == "password"
diff --git a/apps/accounts/automations/push_account/host/windows/manifest.yml b/apps/accounts/automations/push_account/host/windows/manifest.yml
new file mode 100644
index 000000000..05e3127f9
--- /dev/null
+++ b/apps/accounts/automations/push_account/host/windows/manifest.yml
@@ -0,0 +1,13 @@
+id: push_account_local_windows
+name: Push account local account for Windows
+version: 1
+method: push_account
+category: host
+type:
+ - windows
+params:
+ - name: groups
+ type: str
+ label: '用户组'
+ default: 'Users,Remote Desktop Users'
+ help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py
index 92fde0b37..fe117f018 100644
--- a/apps/accounts/automations/push_account/manager.py
+++ b/apps/accounts/automations/push_account/manager.py
@@ -31,6 +31,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
print(msg)
return inventory_hosts
+ host['ssh_params'] = {}
for account in accounts:
h = deepcopy(host)
secret_type = account.secret_type
@@ -49,7 +50,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret)
- h['kwargs'] = self.get_kwargs(account, new_secret, secret_type)
+ h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
h['account'] = {
'name': account.name,
'username': account.username,
diff --git a/apps/accounts/const/account.py b/apps/accounts/const/account.py
index 109044934..55fa02d80 100644
--- a/apps/accounts/const/account.py
+++ b/apps/accounts/const/account.py
@@ -18,3 +18,10 @@ class AliasAccount(TextChoices):
class Source(TextChoices):
LOCAL = 'local', _('Local')
COLLECTED = 'collected', _('Collected')
+ TEMPLATE = 'template', _('Template')
+
+
+class AccountInvalidPolicy(TextChoices):
+ SKIP = 'skip', _('Skip')
+ UPDATE = 'update', _('Update')
+ ERROR = 'error', _('Failed')
diff --git a/apps/accounts/migrations/0010_gatheraccountsautomation_is_sync_account.py b/apps/accounts/migrations/0010_gatheraccountsautomation_is_sync_account.py
new file mode 100644
index 000000000..058a7dd7e
--- /dev/null
+++ b/apps/accounts/migrations/0010_gatheraccountsautomation_is_sync_account.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.16 on 2023-03-23 08:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0009_account_usernames_to_ids'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gatheraccountsautomation',
+ name='is_sync_account',
+ field=models.BooleanField(blank=True, default=False, verbose_name='Is sync account'),
+ ),
+ ]
diff --git a/apps/accounts/migrations/0011_account_source_id.py b/apps/accounts/migrations/0011_account_source_id.py
new file mode 100644
index 000000000..ff9734404
--- /dev/null
+++ b/apps/accounts/migrations/0011_account_source_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.17 on 2023-03-23 07:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0010_gatheraccountsautomation_is_sync_account'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='account',
+ name='source_id',
+ field=models.CharField(max_length=128, null=True, blank=True, verbose_name='Source ID'),
+ ),
+ ]
diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py
index 008318c7e..4094018e1 100644
--- a/apps/accounts/models/account.py
+++ b/apps/accounts/models/account.py
@@ -53,6 +53,7 @@ class Account(AbsConnectivity, BaseAccount):
version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
+ source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
class Meta:
verbose_name = _('Account')
diff --git a/apps/accounts/models/automations/gather_account.py b/apps/accounts/models/automations/gather_account.py
index 01f903141..dd9b5c862 100644
--- a/apps/accounts/models/automations/gather_account.py
+++ b/apps/accounts/models/automations/gather_account.py
@@ -1,7 +1,9 @@
from django.db import models
+from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
-from accounts.const import AutomationTypes
+from accounts.const import AutomationTypes, Source
+from accounts.models import Account
from orgs.mixins.models import JMSOrgBaseModel
from .base import AccountBaseAutomation
@@ -19,6 +21,25 @@ class GatheredAccount(JMSOrgBaseModel):
def address(self):
return self.asset.address
+ @staticmethod
+ def sync_accounts(gathered_accounts):
+ account_objs = []
+ for gathered_account in gathered_accounts:
+ asset_id = gathered_account.asset_id
+ username = gathered_account.username
+ accounts = Account.objects.filter(
+ Q(asset_id=asset_id, username=username) |
+ Q(asset_id=asset_id, name=username)
+ )
+ if accounts.exists():
+ continue
+ account = Account(
+ asset_id=asset_id, username=username,
+ name=username, source=Source.COLLECTED
+ )
+ account_objs.append(account)
+ Account.objects.bulk_create(account_objs)
+
class Meta:
verbose_name = _('Gather account automation')
unique_together = [
@@ -31,6 +52,17 @@ class GatheredAccount(JMSOrgBaseModel):
class GatherAccountsAutomation(AccountBaseAutomation):
+ is_sync_account = models.BooleanField(
+ default=False, blank=True, verbose_name=_("Is sync account")
+ )
+
+ def to_attr_json(self):
+ attr_json = super().to_attr_json()
+ attr_json.update({
+ 'is_sync_account': self.is_sync_account,
+ })
+ return attr_json
+
def save(self, *args, **kwargs):
self.type = AutomationTypes.gather_accounts
super().save(*args, **kwargs)
diff --git a/apps/accounts/models/automations/push_account.py b/apps/accounts/models/automations/push_account.py
index f189a5fbd..d3a14411f 100644
--- a/apps/accounts/models/automations/push_account.py
+++ b/apps/accounts/models/automations/push_account.py
@@ -51,7 +51,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
def to_attr_json(self):
attr_json = super().to_attr_json()
attr_json.update({
- 'username': self.username
+ 'username': self.username,
+ 'params': self.params,
})
return attr_json
diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py
index 5b7bbdbce..b4c5076cc 100644
--- a/apps/accounts/serializers/account/account.py
+++ b/apps/accounts/serializers/account/account.py
@@ -1,75 +1,172 @@
+import uuid
+
+from django.db import IntegrityError
+from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
+from rest_framework.validators import UniqueTogetherValidator
-from accounts.const import SecretType, Source
+from accounts.const import SecretType, Source, AccountInvalidPolicy
from accounts.models import Account, AccountTemplate
from accounts.tasks import push_accounts_to_assets_task
from assets.const import Category, AllTypes
from assets.models import Asset
-from common.serializers import SecretReadableMixin, BulkModelSerializer
+from common.serializers import SecretReadableMixin
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
+from common.utils import get_logger
from .base import BaseAccountSerializer
+logger = get_logger(__name__)
-class AccountSerializerCreateValidateMixin:
- from_id: str
- template: bool
- push_now: bool
- replace_attrs: callable
- def to_internal_value(self, data):
- from_id = data.pop('id', None)
- ret = super().to_internal_value(data)
- self.from_id = from_id
- return ret
+class AccountCreateUpdateSerializerMixin(serializers.Serializer):
+ template = serializers.PrimaryKeyRelatedField(
+ queryset=AccountTemplate.objects,
+ required=False, label=_("Template"), write_only=True
+ )
+ push_now = serializers.BooleanField(
+ default=False, label=_("Push now"), write_only=True
+ )
+ params = serializers.JSONField(
+ decoder=None, encoder=None, required=False, style={'base_template': 'textarea.html'}
+ )
+ on_invalid = LabeledChoiceField(
+ choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
+ write_only=True, label=_('Exist policy')
+ )
- def set_secret(self, attrs):
- _id = self.from_id
- template = attrs.pop('template', None)
+ class Meta:
+ fields = ['template', 'push_now', 'params', 'on_invalid']
- if _id and template:
- account_template = AccountTemplate.objects.get(id=_id)
- attrs['secret'] = account_template.secret
- elif _id and not template:
- account = Account.objects.get(id=_id)
- attrs['secret'] = account.secret
- return attrs
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set_initial_value()
+
+ def set_initial_value(self):
+ if not getattr(self, 'initial_data', None):
+ return
+ if isinstance(self.initial_data, dict):
+ initial_data = [self.initial_data]
+ else:
+ initial_data = self.initial_data
+
+ for data in initial_data:
+ if not data.get('asset') and not self.instance:
+ raise serializers.ValidationError({'asset': 'Asset is required'})
+ asset = data.get('asset') or self.instance.asset
+ self.from_template_if_need(data)
+ self.set_uniq_name_if_need(data, asset)
+
+ def set_uniq_name_if_need(self, initial_data, asset):
+ name = initial_data.get('name')
+ if name is None:
+ return
+ if not name:
+ name = initial_data.get('username')
+ if self.instance and self.instance.name == name:
+ return
+ if Account.objects.filter(name=name, asset=asset).exists():
+ name = name + '_' + uuid.uuid4().hex[:4]
+ initial_data['name'] = name
+
+ @staticmethod
+ def from_template_if_need(initial_data):
+ template_id = initial_data.get('template')
+ if not template_id:
+ return
+ if isinstance(template_id, (str, uuid.UUID)):
+ template = AccountTemplate.objects.filter(id=template_id).first()
+ else:
+ template = template_id
+ if not template:
+ raise serializers.ValidationError({'template': 'Template not found'})
+
+ # Set initial data from template
+ ignore_fields = ['id', 'date_created', 'date_updated', 'org_id']
+ field_names = [
+ field.name for field in template._meta.fields
+ if field.name not in ignore_fields
+ ]
+ attrs = {}
+ for name in field_names:
+ value = getattr(template, name, None)
+ if value is None:
+ continue
+ attrs[name] = value
+ initial_data.update(attrs)
+
+ @staticmethod
+ def push_account_if_need(instance, push_now, params, stat):
+ if not push_now or stat != 'created':
+ return
+ push_accounts_to_assets_task.delay([str(instance.id)], params)
+
+ def get_validators(self):
+ _validators = super().get_validators()
+ if getattr(self, 'initial_data', None) is None:
+ return _validators
+ on_invalid = self.initial_data.get('on_invalid')
+ if on_invalid == AccountInvalidPolicy.ERROR:
+ return _validators
+ _validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)]
+ return _validators
+
+ @staticmethod
+ def do_create(vd):
+ on_invalid = vd.pop('on_invalid', None)
+
+ q = Q()
+ if vd.get('name'):
+ q |= Q(name=vd['name'])
+ if vd.get('username'):
+ q |= Q(username=vd['username'], secret_type=vd.get('secret_type'))
+
+ instance = Account.objects.filter(asset=vd['asset']).filter(q).first()
+ # 不存在这个资产,不用关系策略
+ if not instance:
+ instance = Account.objects.create(**vd)
+ return instance, 'created'
+
+ if on_invalid == AccountInvalidPolicy.SKIP:
+ return instance, 'skipped'
+ elif on_invalid == AccountInvalidPolicy.UPDATE:
+ for k, v in vd.items():
+ setattr(instance, k, v)
+ instance.save()
+ return instance, 'updated'
+ else:
+ raise serializers.ValidationError('Account already exists')
def validate(self, attrs):
attrs = super().validate(attrs)
- return self.set_secret(attrs)
+ if self.instance:
+ return attrs
- @staticmethod
- def push_account(instance, push_now):
- if not push_now:
- return
- push_accounts_to_assets_task.delay([str(instance.id)])
+ template = attrs.pop('template', None)
+ if template:
+ attrs['source'] = Source.TEMPLATE
+ attrs['source_id'] = str(template.id)
+ return attrs
def create(self, validated_data):
push_now = validated_data.pop('push_now', None)
- instance = super().create(validated_data)
- self.push_account(instance, push_now)
+ params = validated_data.pop('params', None)
+ instance, stat = self.do_create(validated_data)
+ self.push_account_if_need(instance, push_now, params, stat)
return instance
def update(self, instance, validated_data):
# account cannot be modified
validated_data.pop('username', None)
+ validated_data.pop('on_invalid', None)
push_now = validated_data.pop('push_now', None)
+ params = validated_data.pop('params', None)
+ validated_data['source_id'] = None
instance = super().update(instance, validated_data)
- self.push_account(instance, push_now)
+ self.push_account_if_need(instance, push_now, params, 'updated')
return instance
-class AccountSerializerCreateMixin(AccountSerializerCreateValidateMixin, BulkModelSerializer):
- template = serializers.BooleanField(
- default=False, label=_("Template"), write_only=True
- )
- push_now = serializers.BooleanField(
- default=False, label=_("Push now"), write_only=True
- )
- has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
-
-
class AccountAssetSerializer(serializers.ModelSerializer):
platform = ObjectRelatedField(read_only=True)
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
@@ -77,11 +174,11 @@ class AccountAssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
- fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info']
+ fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
def to_internal_value(self, data):
if isinstance(data, dict):
- i = data.get('id')
+ i = data.get('id') or data.get('pk')
else:
i = data
@@ -91,9 +188,10 @@ class AccountAssetSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(_('Asset not found'))
-class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
+class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
asset = AccountAssetSerializer(label=_('Asset'))
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
+ has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
su_from = ObjectRelatedField(
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
label=_('Su from'), attrs=('id', 'name', 'username')
@@ -102,27 +200,179 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
class Meta(BaseAccountSerializer.Meta):
model = Account
fields = BaseAccountSerializer.Meta.fields + [
- 'su_from', 'asset', 'template', 'version',
- 'push_now', 'source', 'connectivity',
+ 'su_from', 'asset', 'version',
+ 'source', 'source_id', 'connectivity',
+ ] + AccountCreateUpdateSerializerMixin.Meta.fields
+ read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
+ 'source', 'source_id', 'connectivity'
]
extra_kwargs = {
**BaseAccountSerializer.Meta.extra_kwargs,
- 'name': {'required': False, 'allow_null': True},
+ 'name': {'required': False},
}
- def validate_name(self, value):
- if not value:
- value = self.initial_data.get('username')
- return value
-
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
- queryset = queryset \
- .prefetch_related('asset', 'asset__platform', 'asset__platform__automation')
+ queryset = queryset.prefetch_related(
+ 'asset', 'asset__platform',
+ 'asset__platform__automation'
+ )
return queryset
+class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
+ asset = serializers.CharField(read_only=True, label=_('Asset'))
+ state = serializers.CharField(read_only=True, label=_('State'))
+ error = serializers.CharField(read_only=True, label=_('Error'))
+ changed = serializers.BooleanField(read_only=True, label=_('Changed'))
+
+
+class AssetAccountBulkSerializer(AccountCreateUpdateSerializerMixin, serializers.ModelSerializer):
+ assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
+
+ class Meta:
+ model = Account
+ fields = [
+ 'name', 'username', 'secret', 'secret_type',
+ 'privileged', 'is_active', 'comment', 'template',
+ 'on_invalid', 'push_now', 'assets',
+ ]
+ extra_kwargs = {
+ 'name': {'required': False},
+ 'secret_type': {'required': False},
+ }
+
+ def set_initial_value(self):
+ if not getattr(self, 'initial_data', None):
+ return
+ initial_data = self.initial_data
+ self.from_template_if_need(initial_data)
+
+ @staticmethod
+ def get_filter_lookup(vd):
+ return {
+ 'username': vd['username'],
+ 'secret_type': vd['secret_type'],
+ 'asset': vd['asset'],
+ }
+
+ @staticmethod
+ def get_uniq_name(vd):
+ return vd['name'] + '-' + uuid.uuid4().hex[:4]
+
+ @staticmethod
+ def _handle_update_create(vd, lookup):
+ ori = Account.objects.filter(**lookup).first()
+ if ori and ori.secret == vd['secret']:
+ return ori, False, 'skipped'
+
+ instance, value = Account.objects.update_or_create(defaults=vd, **lookup)
+ state = 'created' if value else 'updated'
+ return instance, True, state
+
+ @staticmethod
+ def _handle_skip_create(vd, lookup):
+ instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
+ state = 'created' if value else 'skipped'
+ return instance, value, state
+
+ @staticmethod
+ def _handle_err_create(vd, lookup):
+ instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
+ if not value:
+ raise serializers.ValidationError(_('Account already exists'))
+ return instance, True, 'created'
+
+ def perform_create(self, vd, handler):
+ lookup = self.get_filter_lookup(vd)
+ try:
+ instance, changed, state = handler(vd, lookup)
+ except IntegrityError:
+ vd['name'] = self.get_uniq_name(vd)
+ instance, changed, state = handler(vd, lookup)
+ return instance, changed, state
+
+ def get_create_handler(self, on_invalid):
+ if on_invalid == 'update':
+ handler = self._handle_update_create
+ elif on_invalid == 'skip':
+ handler = self._handle_skip_create
+ else:
+ handler = self._handle_err_create
+ return handler
+
+ def perform_bulk_create(self, vd):
+ assets = vd.pop('assets')
+ on_invalid = vd.pop('on_invalid', 'skip')
+ secret_type = vd.get('secret_type', 'password')
+
+ if not vd.get('name'):
+ vd['name'] = vd.get('username')
+
+ create_handler = self.get_create_handler(on_invalid)
+ asset_ids = [asset.id for asset in assets]
+ secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
+
+ _results = {}
+ for asset in assets:
+ if asset not in secret_type_supports:
+ _results[asset] = {
+ 'error': _('Asset does not support this secret type: %s') % secret_type,
+ 'state': 'error',
+ }
+ continue
+
+ vd = vd.copy()
+ vd['asset'] = asset
+ try:
+ instance, changed, state = self.perform_create(vd, create_handler)
+ _results[asset] = {
+ 'changed': changed, 'instance': instance.id, 'state': state
+ }
+ except serializers.ValidationError as e:
+ _results[asset] = {'error': e.detail[0], 'state': 'error'}
+ except Exception as e:
+ logger.exception(e)
+ _results[asset] = {'error': str(e), 'state': 'error'}
+
+ results = [{'asset': asset, **result} for asset, result in _results.items()]
+ state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
+ results = sorted(results, key=lambda x: state_score.get(x['state'], 4))
+
+ if on_invalid != 'error':
+ return results
+
+ errors = []
+ errors.extend([result for result in results if result['state'] == 'error'])
+ for result in results:
+ if result['state'] != 'skipped':
+ continue
+ errors.append({
+ 'error': _('Account has exist'),
+ 'state': 'error',
+ 'asset': str(result['asset'])
+ })
+ if errors:
+ raise serializers.ValidationError(errors)
+ return results
+
+ @staticmethod
+ def push_accounts_if_need(results, push_now):
+ if not push_now:
+ return
+ accounts = [str(v['instance']) for v in results if v.get('instance')]
+ push_accounts_to_assets_task.delay(accounts)
+
+ def create(self, validated_data):
+ push_now = validated_data.pop('push_now', False)
+ results = self.perform_bulk_create(validated_data)
+ self.push_accounts_if_need(results, push_now)
+ for res in results:
+ res['asset'] = str(res['asset'])
+ return results
+
+
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class Meta(AccountSerializer.Meta):
extra_kwargs = {
diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py
index f31f19196..4e2b1a1df 100644
--- a/apps/accounts/serializers/account/base.py
+++ b/apps/accounts/serializers/account/base.py
@@ -13,10 +13,10 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
class AuthValidateMixin(serializers.Serializer):
secret_type = LabeledChoiceField(
- choices=SecretType.choices, required=True, label=_('Secret type')
+ choices=SecretType.choices, label=_('Secret type'), default='password'
)
secret = EncryptedField(
- label=_('Secret/Password'), required=False, max_length=40960, allow_blank=True,
+ label=_('Secret'), required=False, max_length=40960, allow_blank=True,
allow_null=True, write_only=True,
)
passphrase = serializers.CharField(
@@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
'date_verified', 'created_by', 'date_created',
]
extra_kwargs = {
- 'name': {'required': True},
'spec_info': {'label': _('Spec info')},
}
diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py
index a72565cf5..45190f051 100644
--- a/apps/accounts/serializers/account/template.py
+++ b/apps/accounts/serializers/account/template.py
@@ -1,4 +1,5 @@
-from accounts.models import AccountTemplate
+from accounts.models import AccountTemplate, Account
+from assets.models import Asset
from common.serializers import SecretReadableMixin
from .base import BaseAccountSerializer
@@ -7,17 +8,47 @@ class AccountTemplateSerializer(BaseAccountSerializer):
class Meta(BaseAccountSerializer.Meta):
model = AccountTemplate
- # @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)
+ @staticmethod
+ def bulk_update_accounts(instance, diff):
+ accounts = Account.objects.filter(source_id=instance.id)
+ if not accounts:
+ return
+
+ secret_type = diff.pop('secret_type', None)
+ diff.pop('secret', None)
+ update_accounts = []
+ for account in accounts:
+ for field, value in diff.items():
+ setattr(account, field, value)
+ update_accounts.append(account)
+ if update_accounts:
+ Account.objects.bulk_update(update_accounts, diff.keys())
+
+ if secret_type is None:
+ return
+
+ update_accounts = []
+ asset_ids = accounts.values_list('asset_id', flat=True)
+ secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
+ asset_ids_supports = [asset.id for asset in secret_type_supports]
+ for account in accounts:
+ asset_id = account.asset_id
+ if asset_id not in asset_ids_supports:
+ continue
+ account.secret_type = secret_type
+ account.secret = instance.secret
+ update_accounts.append(account)
+ if update_accounts:
+ Account.objects.bulk_update(update_accounts, ['secret', 'secret_type'])
+
+ def update(self, instance, validated_data):
+ diff = {
+ k: v for k, v in validated_data.items()
+ if getattr(instance, k) != v
+ }
+ instance = super().update(instance, validated_data)
+ self.bulk_update_accounts(instance, diff)
+ return instance
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
diff --git a/apps/accounts/serializers/automations/gather_accounts.py b/apps/accounts/serializers/automations/gather_accounts.py
index b906e7881..fd09773de 100644
--- a/apps/accounts/serializers/automations/gather_accounts.py
+++ b/apps/accounts/serializers/automations/gather_accounts.py
@@ -17,7 +17,8 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
class Meta:
model = GatherAccountsAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
- fields = BaseAutomationSerializer.Meta.fields + read_only_fields
+ fields = BaseAutomationSerializer.Meta.fields \
+ + ['is_sync_account'] + read_only_fields
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
diff --git a/apps/accounts/serializers/automations/push_account.py b/apps/accounts/serializers/automations/push_account.py
index b9982300b..1d7bb3d36 100644
--- a/apps/accounts/serializers/automations/push_account.py
+++ b/apps/accounts/serializers/automations/push_account.py
@@ -7,9 +7,10 @@ from .change_secret import (
class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
+
class Meta(ChangeSecretAutomationSerializer.Meta):
model = PushAccountAutomation
- fields = [
+ fields = ['params'] + [
n for n in ChangeSecretAutomationSerializer.Meta.fields
if n not in ['recipients']
]
diff --git a/apps/accounts/signal_handlers.py b/apps/accounts/signal_handlers.py
index b47588192..cf09842cc 100644
--- a/apps/accounts/signal_handlers.py
+++ b/apps/accounts/signal_handlers.py
@@ -8,8 +8,8 @@ logger = get_logger(__name__)
@receiver(pre_save, sender=Account)
-def on_account_pre_save(sender, instance, created=False, **kwargs):
- if created:
+def on_account_pre_save(sender, instance, **kwargs):
+ if instance.version == 0:
instance.version = 1
else:
instance.version = instance.history.count()
diff --git a/apps/accounts/tasks/push_account.py b/apps/accounts/tasks/push_account.py
index 2a753cc1a..623481be9 100644
--- a/apps/accounts/tasks/push_account.py
+++ b/apps/accounts/tasks/push_account.py
@@ -15,7 +15,7 @@ __all__ = [
queue="ansible", verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
)
-def push_accounts_to_assets_task(account_ids):
+def push_accounts_to_assets_task(account_ids, params=None):
from accounts.models import PushAccountAutomation
from accounts.models import Account
@@ -26,6 +26,7 @@ def push_accounts_to_assets_task(account_ids):
task_snapshot = {
'accounts': [str(account.id) for account in accounts],
'assets': [str(account.asset_id) for account in accounts],
+ 'params': params or {},
}
tp = AutomationTypes.push_account
diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py
index 59a70b3cf..5c57ad67b 100644
--- a/apps/accounts/urls.py
+++ b/apps/accounts/urls.py
@@ -25,6 +25,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
urlpatterns = [
+ path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),
path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
path('account-secrets//histories/', api.AccountHistoriesSecretAPI.as_view(),
name='account-secret-history'),
diff --git a/apps/assets/api/asset/__init__.py b/apps/assets/api/asset/__init__.py
index c20e44573..0f1d81825 100644
--- a/apps/assets/api/asset/__init__.py
+++ b/apps/assets/api/asset/__init__.py
@@ -1,7 +1,8 @@
from .asset import *
-from .host import *
-from .database import *
-from .web import *
from .cloud import *
+from .custom import *
+from .database import *
from .device import *
+from .host import *
from .permission import *
+from .web import *
diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py
index c917e09d9..aa20bded9 100644
--- a/apps/assets/api/asset/asset.py
+++ b/apps/assets/api/asset/asset.py
@@ -2,15 +2,17 @@
#
import django_filters
from django.db.models import Q
+from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from rest_framework.decorators import action
from rest_framework.response import Response
+from rest_framework.status import HTTP_200_OK
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
from assets import serializers
from assets.exceptions import NotSupportedTemporarilyError
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
-from assets.models import Asset, Gateway
+from assets.models import Asset, Gateway, Platform
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
from common.api import SuggestionMixin
from common.drf.filters import BaseFilterSet
@@ -18,6 +20,7 @@ from common.utils import get_logger, is_uuid
from orgs.mixins import generics
from orgs.mixins.api import OrgBulkModelViewSet
from ..mixin import NodeFilterMixin
+from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
logger = get_logger(__file__)
__all__ = [
@@ -99,16 +102,16 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
("platform", serializers.PlatformSerializer),
("suggestion", serializers.MiniAssetSerializer),
("gateways", serializers.GatewaySerializer),
- ("spec_info", serializers.SpecSerializer),
)
rbac_perms = (
("match", "assets.match_asset"),
("platform", "assets.view_platform"),
("gateways", "assets.view_gateway"),
("spec_info", "assets.view_asset"),
- ("info", "assets.view_asset"),
+ ("gathered_info", "assets.view_asset"),
)
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
+ skip_assets = []
def get_serializer_class(self):
cls = super().get_serializer_class()
@@ -124,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
serializer = super().get_serializer(instance=asset.platform)
return Response(serializer.data)
- @action(methods=["GET"], detail=True, url_path="spec-info")
- def spec_info(self, *args, **kwargs):
- asset = super().get_object()
- return Response(asset.spec_info)
-
@action(methods=["GET"], detail=True, url_path="gateways")
def gateways(self, *args, **kwargs):
asset = self.get_object()
@@ -144,6 +142,31 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs)
+ def filter_bulk_update_data(self):
+ bulk_data = []
+ for data in self.request.data:
+ pk = data.get('id')
+ platform = data.get('platform')
+ if not platform:
+ bulk_data.append(data)
+ continue
+ asset = get_object_or_404(Asset, pk=pk)
+ platform = get_object_or_404(Platform, **platform)
+ if platform.type == asset.type:
+ bulk_data.append(data)
+ continue
+ self.skip_assets.append(asset)
+ return bulk_data
+
+ def bulk_update(self, request, *args, **kwargs):
+ bulk_data = self.filter_bulk_update_data()
+ request._full_data = bulk_data
+ response = super().bulk_update(request, *args, **kwargs)
+ if response.status_code == HTTP_200_OK and self.skip_assets:
+ user = request.user
+ BulkUpdatePlatformSkipAssetUserMsg(user, self.skip_assets).publish()
+ return response
+
class AssetsTaskMixin:
def perform_assets_task(self, serializer):
@@ -154,8 +177,8 @@ class AssetsTaskMixin:
task = update_assets_hardware_info_manual(assets)
else:
asset = assets[0]
- if not asset.auto_info['ansible_enabled'] or \
- not asset.auto_info['ping_enabled']:
+ if not asset.auto_config['ansible_enabled'] or \
+ not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError()
task = test_assets_connectivity_manual(assets)
return task
diff --git a/apps/assets/api/asset/custom.py b/apps/assets/api/asset/custom.py
new file mode 100644
index 000000000..ca5058ed6
--- /dev/null
+++ b/apps/assets/api/asset/custom.py
@@ -0,0 +1,16 @@
+from assets.models import Custom, Asset
+from assets.serializers import CustomSerializer
+
+from .asset import AssetViewSet
+
+__all__ = ['CustomViewSet']
+
+
+class CustomViewSet(AssetViewSet):
+ model = Custom
+ perm_model = Asset
+
+ def get_serializer_classes(self):
+ serializer_classes = super().get_serializer_classes()
+ serializer_classes['default'] = CustomSerializer
+ return serializer_classes
diff --git a/apps/assets/api/asset/host.py b/apps/assets/api/asset/host.py
index b92448bfb..d2ddc954d 100644
--- a/apps/assets/api/asset/host.py
+++ b/apps/assets/api/asset/host.py
@@ -1,8 +1,5 @@
-from rest_framework.decorators import action
-from rest_framework.response import Response
-
from assets.models import Host, Asset
-from assets.serializers import HostSerializer, HostInfoSerializer
+from assets.serializers import HostSerializer
from .asset import AssetViewSet
__all__ = ['HostViewSet']
@@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet):
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = HostSerializer
- serializer_classes['info'] = HostInfoSerializer
return serializer_classes
-
- @action(methods=["GET"], detail=True, url_path="info")
- def info(self, *args, **kwargs):
- asset = super().get_object()
- serializer = self.get_serializer(asset.info)
- data = serializer.data
- data['asset'] = {
- 'id': asset.id, 'name': asset.name,
- 'address': asset.address
- }
- return Response(data)
diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py
index 6cc198169..d7cb90121 100644
--- a/apps/assets/api/mixin.py
+++ b/apps/assets/api/mixin.py
@@ -8,6 +8,15 @@ from common.utils import lazyproperty, timeit
class SerializeToTreeNodeMixin:
+ request: Request
+
+ @lazyproperty
+ def is_sync(self):
+ sync_paths = ['/api/v1/perms/users/self/nodes/all-with-assets/tree/']
+ for p in sync_paths:
+ if p == self.request.path:
+ return True
+ return False
@timeit
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
@@ -17,6 +26,16 @@ class SerializeToTreeNodeMixin:
else:
def _name(node: Node):
return node.value
+
+ def _open(node):
+ if not self.is_sync:
+ # 异步加载资产树时,默认展开节点
+ return True
+ if not node.parent_key:
+ return True
+ else:
+ return False
+
data = [
{
'id': node.key,
@@ -24,7 +43,7 @@ class SerializeToTreeNodeMixin:
'title': _name(node),
'pId': node.parent_key,
'isParent': True,
- 'open': True,
+ 'open': _open(node),
'meta': {
'data': {
"id": node.id,
@@ -52,7 +71,7 @@ class SerializeToTreeNodeMixin:
{
'id': str(asset.id),
'name': asset.name,
- 'title': asset.address,
+ 'title': f'{asset.address}\n{asset.comment}',
'pId': get_pid(asset),
'isParent': False,
'open': False,
@@ -64,6 +83,8 @@ class SerializeToTreeNodeMixin:
'platform_type': asset.platform.type,
'org_name': asset.org_name,
'sftp': asset.platform_id in sftp_enabled_platform,
+ 'name': asset.name,
+ 'address': asset.address
},
}
}
diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py
index be9456ff7..61f4db985 100644
--- a/apps/assets/api/platform.py
+++ b/apps/assets/api/platform.py
@@ -1,10 +1,16 @@
+from rest_framework import generics
+from rest_framework import serializers
+from rest_framework.decorators import action
+from rest_framework.response import Response
+
from assets.const import AllTypes
-from assets.models import Platform
+from assets.models import Platform, Node, Asset
from assets.serializers import PlatformSerializer
from common.api import JMSModelViewSet
+from common.permissions import IsValidUser
from common.serializers import GroupedChoiceSerializer
-__all__ = ['AssetPlatformViewSet']
+__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi']
class AssetPlatformViewSet(JMSModelViewSet):
@@ -18,12 +24,13 @@ class AssetPlatformViewSet(JMSModelViewSet):
rbac_perms = {
'categories': 'assets.view_platform',
'type_constraints': 'assets.view_platform',
- 'ops_methods': 'assets.view_platform'
+ 'ops_methods': 'assets.view_platform',
+ 'filter_nodes_assets': 'assets.view_platform'
}
def get_queryset(self):
queryset = super().get_queryset()
- queryset = queryset.filter(type__in=AllTypes.get_types())
+ queryset = queryset.filter(type__in=AllTypes.get_types_values())
return queryset
def get_object(self):
@@ -38,3 +45,44 @@ class AssetPlatformViewSet(JMSModelViewSet):
request, message={"detail": "Internal platform"}
)
return super().check_object_permissions(request, obj)
+
+ @action(methods=['post'], detail=False, url_path='filter-nodes-assets')
+ def filter_nodes_assets(self, request, *args, **kwargs):
+ node_ids = request.data.get('node_ids', [])
+ asset_ids = request.data.get('asset_ids', [])
+ nodes = Node.objects.filter(id__in=node_ids)
+ node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
+ direct_asset_ids = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True)
+ platform_ids = Asset.objects.filter(
+ id__in=set(list(direct_asset_ids) + list(node_asset_ids))
+ ).values_list('platform_id', flat=True)
+ platforms = Platform.objects.filter(id__in=platform_ids)
+ serializer = self.get_serializer(platforms, many=True)
+ return Response(serializer.data)
+
+
+class PlatformAutomationMethodsApi(generics.ListAPIView):
+ permission_classes = (IsValidUser,)
+
+ @staticmethod
+ def automation_methods():
+ return AllTypes.get_automation_methods()
+
+ def generate_serializer_fields(self):
+ data = self.automation_methods()
+ fields = {
+ i['id']: i['params_serializer']()
+ if i['params_serializer'] else None
+ for i in data
+ }
+ return fields
+
+ def get_serializer_class(self):
+ fields = self.generate_serializer_fields()
+ serializer_name = 'AutomationMethodsSerializer'
+ return type(serializer_name, (serializers.Serializer,), fields)
+
+ def list(self, request, *args, **kwargs):
+ data = self.generate_serializer_fields()
+ serializer = self.get_serializer(data)
+ return Response(serializer.data)
diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py
index 7184d91f7..9207bb33e 100644
--- a/apps/assets/automations/base/manager.py
+++ b/apps/assets/automations/base/manager.py
@@ -41,6 +41,26 @@ class BasePlaybookManager:
self.method_hosts_mapper = defaultdict(list)
self.playbooks = []
self.gateway_servers = dict()
+ params = self.execution.snapshot.get('params')
+ self.params = params or {}
+
+ def get_params(self, automation, method_type):
+ method_attr = '{}_method'.format(method_type)
+ method_params = '{}_params'.format(method_type)
+ method_id = getattr(automation, method_attr)
+ automation_params = getattr(automation, method_params)
+ serializer = self.method_id_meta_mapper[method_id]['params_serializer']
+
+ if serializer is None:
+ return {}
+
+ data = self.params.get(method_id, {})
+ params = serializer(data).data
+ return {
+ field_name: automation_params.get(field_name, '')
+ if not params[field_name] else params[field_name]
+ for field_name in params
+ }
@property
def platform_automation_methods(self):
@@ -101,8 +121,9 @@ class BasePlaybookManager:
return host
def host_callback(self, host, automation=None, **kwargs):
- enabled_attr = '{}_enabled'.format(self.__class__.method_type())
- method_attr = '{}_method'.format(self.__class__.method_type())
+ method_type = self.__class__.method_type()
+ enabled_attr = '{}_enabled'.format(method_type)
+ method_attr = '{}_method'.format(method_type)
method_enabled = automation and \
getattr(automation, enabled_attr) and \
@@ -114,6 +135,7 @@ class BasePlaybookManager:
return host
host = self.convert_cert_to_file(host, kwargs.get('path_dir'))
+ host['params'] = self.get_params(automation, method_type)
return host
@staticmethod
@@ -239,10 +261,12 @@ class BasePlaybookManager:
jms_asset, jms_gateway = host['jms_asset'], host.get('gateway')
if not jms_gateway:
continue
+
server = SSHTunnelForwarder(
(jms_gateway['address'], jms_gateway['port']),
ssh_username=jms_gateway['username'],
ssh_password=jms_gateway['secret'],
+ ssh_pkey=jms_gateway['private_key_path'],
remote_bind_address=(jms_asset['address'], jms_asset['port'])
)
try:
@@ -252,8 +276,8 @@ class BasePlaybookManager:
print('\033[31m %s \033[0m\n' % err_msg)
not_valid.append(k)
else:
- jms_asset['address'] = '127.0.0.1'
- jms_asset['port'] = server.local_bind_port
+ host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
+ host['ansible_port'] = jms_asset['port'] = server.local_bind_port
servers.append(server)
# 网域不可连接的,就不继续执行此资源的后续任务了
diff --git a/apps/assets/automations/gather_facts/manager.py b/apps/assets/automations/gather_facts/manager.py
index afd5ce523..e33d97617 100644
--- a/apps/assets/automations/gather_facts/manager.py
+++ b/apps/assets/automations/gather_facts/manager.py
@@ -29,7 +29,7 @@ class GatherFactsManager(BasePlaybookManager):
asset = self.host_asset_mapper.get(host)
if asset and info:
info = self.format_asset_info(asset.type, info)
- asset.info = info
- asset.save(update_fields=['info'])
+ asset.gathered_info = info
+ asset.save(update_fields=['gathered_info'])
else:
logger.error("Not found info: {}".format(host))
diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py
index 86fcb775c..cad2b1b3c 100644
--- a/apps/assets/automations/methods.py
+++ b/apps/assets/automations/methods.py
@@ -1,8 +1,9 @@
-import os
-import yaml
import json
+import os
from functools import partial
+import yaml
+
def check_platform_method(manifest, manifest_path):
required_keys = ['category', 'method', 'name', 'id', 'type']
@@ -21,6 +22,15 @@ def check_platform_methods(methods):
raise ValueError("Duplicate id: {}".format(_id))
+def generate_serializer(data):
+ from common.serializers import create_serializer_class
+ params = data.pop('params', None)
+ if not params:
+ return None
+ serializer_name = data['id'].title().replace('_', '') + 'Serializer'
+ return create_serializer_class(serializer_name, params)
+
+
def get_platform_automation_methods(path):
methods = []
for root, dirs, files in os.walk(path, topdown=False):
@@ -33,6 +43,7 @@ def get_platform_automation_methods(path):
manifest = yaml.safe_load(f)
check_platform_method(manifest, path)
manifest['dir'] = os.path.dirname(path)
+ manifest['params_serializer'] = generate_serializer(manifest)
methods.append(manifest)
check_platform_methods(methods)
@@ -46,12 +57,12 @@ def filter_key(manifest, attr, value):
return value in manifest_value or 'all' in manifest_value
-def filter_platform_methods(category, tp, method=None, methods=None):
+def filter_platform_methods(category, tp_name, method=None, methods=None):
methods = platform_automation_methods if methods is None else methods
if category:
methods = filter(partial(filter_key, attr='category', value=category), methods)
- if tp:
- methods = filter(partial(filter_key, attr='type', value=tp), methods)
+ if tp_name:
+ methods = filter(partial(filter_key, attr='type', value=tp_name), methods)
if method:
methods = filter(lambda x: x['method'] == method, methods)
return methods
diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py
index aa5070d5e..c90ab7320 100644
--- a/apps/assets/const/base.py
+++ b/apps/assets/const/base.py
@@ -4,10 +4,21 @@ from jumpserver.utils import has_valid_xpack_license
from .protocol import Protocol
+class Type:
+ def __init__(self, label, value):
+ self.label = label
+ self.value = value
+
+ def __str__(self):
+ return self.value
+
+
class BaseType(TextChoices):
"""
- 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持
+ 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh,
+ 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持
"""
+
@classmethod
def get_constrains(cls):
constrains = {}
@@ -20,7 +31,7 @@ class BaseType(TextChoices):
protocols_default = protocols.pop('*', {})
automation_default = automation.pop('*', {})
- for k, v in cls.choices:
+ for k, v in cls.get_choices():
tp_base = {**base_default, **base.get(k, {})}
tp_auto = {**automation_default, **automation.get(k, {})}
tp_protocols = {**protocols_default, **protocols.get(k, {})}
@@ -35,8 +46,12 @@ class BaseType(TextChoices):
choices = protocol.get('choices', [])
if choices == '__self__':
choices = [tp]
- protocols = [{'name': name, **settings.get(name, {})} for name in choices]
- protocols[0]['primary'] = True
+ protocols = [
+ {'name': name, **settings.get(name, {})}
+ for name in choices
+ ]
+ if protocols:
+ protocols[0]['default'] = True
return protocols
@classmethod
@@ -56,23 +71,21 @@ class BaseType(TextChoices):
raise NotImplementedError
@classmethod
- def get_community_types(cls):
- raise NotImplementedError
+ def _get_choices_to_types(cls):
+ choices = cls.get_choices()
+ return [Type(label, value) for value, label in choices]
@classmethod
def get_types(cls):
- tps = [tp for tp in cls]
+ tps = cls._get_choices_to_types()
if not has_valid_xpack_license():
tps = cls.get_community_types()
return tps
+ @classmethod
+ def get_community_types(cls):
+ return cls._get_choices_to_types()
+
@classmethod
def get_choices(cls):
- tps = cls.get_types()
- cls_choices = cls.choices
- return [
- choice for choice in cls_choices
- if choice[0] in tps
- ]
-
-
+ return cls.choices
diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py
index e14867d69..8c4d387d8 100644
--- a/apps/assets/const/category.py
+++ b/apps/assets/const/category.py
@@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
from common.db.models import ChoicesMixin
-
__all__ = ['Category']
@@ -13,13 +12,10 @@ class Category(ChoicesMixin, models.TextChoices):
DATABASE = 'database', _("Database")
CLOUD = 'cloud', _("Cloud service")
WEB = 'web', _("Web")
+ CUSTOM = 'custom', _("Custom type")
@classmethod
def filter_choices(cls, category):
_category = getattr(cls, category.upper(), None)
choices = [(_category.value, _category.label)] if _category else cls.choices
return choices
-
-
-
-
diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py
index 12c4f09dd..02410a6a9 100644
--- a/apps/assets/const/cloud.py
+++ b/apps/assets/const/cloud.py
@@ -15,6 +15,9 @@ class CloudTypes(BaseType):
'charset_enabled': False,
'domain_enabled': False,
'su_enabled': False,
+ },
+ cls.K8S: {
+ 'domain_enabled': True,
}
}
diff --git a/apps/assets/const/custom.py b/apps/assets/const/custom.py
new file mode 100644
index 000000000..47d4e9229
--- /dev/null
+++ b/apps/assets/const/custom.py
@@ -0,0 +1,60 @@
+from .base import BaseType
+
+
+class CustomTypes(BaseType):
+ @classmethod
+ def get_choices(cls):
+ try:
+ platforms = list(cls.get_custom_platforms())
+ except Exception:
+ return []
+ types = [p.type for p in platforms]
+ return [(t, t) for t in types]
+
+ @classmethod
+ def _get_base_constrains(cls) -> dict:
+ return {
+ '*': {
+ 'charset_enabled': False,
+ 'domain_enabled': False,
+ 'su_enabled': False,
+ },
+ }
+
+ @classmethod
+ def _get_automation_constrains(cls) -> dict:
+ constrains = {
+ '*': {
+ 'ansible_enabled': False,
+ 'ansible_config': {},
+ 'gather_facts_enabled': False,
+ 'verify_account_enabled': False,
+ 'change_secret_enabled': False,
+ 'push_account_enabled': False,
+ 'gather_accounts_enabled': False,
+ }
+ }
+ return constrains
+
+ @classmethod
+ def _get_protocol_constrains(cls) -> dict:
+ constrains = {}
+ for platform in cls.get_custom_platforms():
+ choices = list(platform.protocols.values_list('name', flat=True))
+ if platform.type in constrains:
+ choices = constrains[platform.type]['choices'] + choices
+ constrains[platform.type] = {'choices': choices}
+ return constrains
+
+ @classmethod
+ def internal_platforms(cls):
+ return {
+ # cls.PUBLIC: [],
+ # cls.PRIVATE: [{'name': 'Vmware-vSphere'}],
+ # cls.K8S: [{'name': 'Kubernetes'}],
+ }
+
+ @classmethod
+ def get_custom_platforms(cls):
+ from assets.models import Platform
+ return Platform.objects.filter(category='custom')
diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py
index d00996dd6..5e8f8f879 100644
--- a/apps/assets/const/device.py
+++ b/apps/assets/const/device.py
@@ -15,7 +15,8 @@ class DeviceTypes(BaseType):
'*': {
'charset_enabled': False,
'domain_enabled': True,
- 'su_enabled': False,
+ 'su_enabled': True,
+ 'su_methods': ['enable', 'super', 'super_level']
}
}
diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py
index eb7930b09..afb92a447 100644
--- a/apps/assets/const/host.py
+++ b/apps/assets/const/host.py
@@ -19,10 +19,7 @@ class HostTypes(BaseType):
'charset': 'utf-8', # default
'domain_enabled': True,
'su_enabled': True,
- 'su_methods': [
- {'name': 'sudo su', 'id': 'sudo su'},
- {'name': 'su -', 'id': 'su -'}
- ],
+ 'su_methods': ['sudo', 'su'],
},
cls.WINDOWS: {
'su_enabled': False,
@@ -39,7 +36,7 @@ class HostTypes(BaseType):
'choices': ['ssh', 'telnet', 'vnc', 'rdp']
},
cls.WINDOWS: {
- 'choices': ['rdp', 'ssh', 'vnc']
+ 'choices': ['rdp', 'ssh', 'vnc', 'winrm']
}
}
@@ -61,7 +58,7 @@ class HostTypes(BaseType):
cls.WINDOWS: {
'ansible_config': {
'ansible_shell_type': 'cmd',
- 'ansible_connection': 'ssh',
+ 'ansible_connection': 'smart',
},
},
cls.OTHER_HOST: {
diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py
index 884a53785..a0b7b7ec7 100644
--- a/apps/assets/const/protocol.py
+++ b/apps/assets/const/protocol.py
@@ -10,6 +10,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
+ winrm = 'winrm', 'WinRM'
mysql = 'mysql', 'MySQL'
mariadb = 'mariadb', 'MariaDB'
@@ -51,6 +52,13 @@ class Protocol(ChoicesMixin, models.TextChoices):
'port': 23,
'secret_types': ['password'],
},
+ cls.winrm: {
+ 'port': 5985,
+ 'secret_types': ['password'],
+ 'setting': {
+ 'use_ssl': False,
+ }
+ },
}
@classmethod
@@ -116,7 +124,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
'setting': {
'username_selector': 'name=username',
'password_selector': 'name=password',
- 'submit_selector': 'id=longin_button',
+ 'submit_selector': 'id=login_button',
}
},
}
@@ -128,3 +136,11 @@ class Protocol(ChoicesMixin, models.TextChoices):
**cls.database_protocols(),
**cls.cloud_protocols()
}
+
+ @classmethod
+ def protocol_secret_types(cls):
+ settings = cls.settings()
+ return {
+ protocol: settings[protocol]['secret_types'] or ['password']
+ for protocol in cls.settings()
+ }
diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py
index 00a496eee..a73b967b2 100644
--- a/apps/assets/const/types.py
+++ b/apps/assets/const/types.py
@@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin
from .category import Category
from .cloud import CloudTypes
+from .custom import CustomTypes
from .database import DatabaseTypes
from .device import DeviceTypes
from .host import HostTypes
@@ -16,7 +17,7 @@ class AllTypes(ChoicesMixin):
choices: list
includes = [
HostTypes, DeviceTypes, DatabaseTypes,
- CloudTypes, WebTypes,
+ CloudTypes, WebTypes, CustomTypes
]
_category_constrains = {}
@@ -24,22 +25,29 @@ class AllTypes(ChoicesMixin):
def choices(cls):
choices = []
for tp in cls.includes:
- choices.extend(tp.choices)
+ choices.extend(tp.get_choices())
return choices
+ @classmethod
+ def get_choices(cls):
+ return cls.choices()
+
@classmethod
def filter_choices(cls, category):
- choices = dict(cls.category_types()).get(category, cls).choices
+ choices = dict(cls.category_types()).get(category, cls).get_choices()
return choices() if callable(choices) else choices
@classmethod
- def get_constraints(cls, category, tp):
+ def get_constraints(cls, category, tp_name):
+ if not isinstance(tp_name, str):
+ tp_name = tp_name.value
+
types_cls = dict(cls.category_types()).get(category)
if not types_cls:
return {}
type_constraints = types_cls.get_constrains()
- constraints = type_constraints.get(tp, {})
- cls.set_automation_methods(category, tp, constraints)
+ constraints = type_constraints.get(tp_name, {})
+ cls.set_automation_methods(category, tp_name, constraints)
return constraints
@classmethod
@@ -56,7 +64,7 @@ class AllTypes(ChoicesMixin):
return asset_methods + account_methods
@classmethod
- def set_automation_methods(cls, category, tp, constraints):
+ def set_automation_methods(cls, category, tp_name, constraints):
from assets.automations import filter_platform_methods
automation = constraints.get('automation', {})
automation_methods = {}
@@ -66,7 +74,7 @@ class AllTypes(ChoicesMixin):
continue
item_name = item.replace('_enabled', '')
methods = filter_platform_methods(
- category, tp, item_name, methods=platform_automation_methods
+ category, tp_name, item_name, methods=platform_automation_methods
)
methods = [{'name': m['name'], 'id': m['id']} for m in methods]
automation_methods[item_name + '_methods'] = methods
@@ -113,7 +121,7 @@ class AllTypes(ChoicesMixin):
@classmethod
def grouped_choices(cls):
- grouped_types = [(str(ca), tp.choices) for ca, tp in cls.category_types()]
+ grouped_types = [(str(ca), tp.get_choices()) for ca, tp in cls.category_types()]
return grouped_types
@classmethod
@@ -136,16 +144,22 @@ class AllTypes(ChoicesMixin):
(Category.HOST, HostTypes),
(Category.DEVICE, DeviceTypes),
(Category.DATABASE, DatabaseTypes),
+ (Category.CLOUD, CloudTypes),
(Category.WEB, WebTypes),
- (Category.CLOUD, CloudTypes)
+ (Category.CUSTOM, CustomTypes),
)
@classmethod
def get_types(cls):
- tps = []
+ choices = []
for i in dict(cls.category_types()).values():
- tps.extend(i.get_types())
- return tps
+ choices.extend(i.get_types())
+ return choices
+
+ @classmethod
+ def get_types_values(cls):
+ choices = cls.get_types()
+ return [c.value for c in choices]
@staticmethod
def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None):
diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py
index cd5732630..87eed5fb0 100644
--- a/apps/assets/migrations/0100_auto_20220711_1413.py
+++ b/apps/assets/migrations/0100_auto_20220711_1413.py
@@ -49,7 +49,10 @@ def migrate_asset_accounts(apps, schema_editor):
account_values.update(auth_book_auth)
auth_infos = []
- username = account_values['username']
+ username = account_values.get('username')
+ if not username:
+ continue
+
for attr in auth_attrs:
secret = account_values.pop(attr, None)
if not secret:
diff --git a/apps/assets/migrations/0111_auto_20230321_1633.py b/apps/assets/migrations/0111_auto_20230321_1633.py
new file mode 100644
index 000000000..52dbc1971
--- /dev/null
+++ b/apps/assets/migrations/0111_auto_20230321_1633.py
@@ -0,0 +1,49 @@
+# Generated by Django 3.2.17 on 2023-03-21 08:33
+
+from django.db import migrations, models
+
+from assets.const import AllTypes
+
+
+def migrate_platform_charset(apps, schema_editor):
+ platform_model = apps.get_model('assets', 'Platform')
+ platform_model.objects.filter(charset='utf8').update(charset='utf-8')
+
+
+def migrate_platform_protocol_primary(apps, schema_editor):
+ platform_model = apps.get_model('assets', 'Platform')
+ platforms = platform_model.objects.all()
+
+ for platform in platforms:
+ p = platform.protocols.first()
+ if not p:
+ continue
+ p.primary = True
+ p.save()
+
+
+def migrate_internal_platforms(apps, schema_editor):
+ platform_cls = apps.get_model('assets', 'Platform')
+ AllTypes.create_or_update_internal_platforms(platform_cls)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('assets', '0110_auto_20230315_1741'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='platformprotocol',
+ name='primary',
+ field=models.BooleanField(default=False, verbose_name='Primary'),
+ ),
+ migrations.AddField(
+ model_name='platformprotocol',
+ name='public',
+ field=models.BooleanField(default=True, verbose_name='Public'),
+ ),
+ migrations.RunPython(migrate_platform_charset),
+ migrations.RunPython(migrate_platform_protocol_primary),
+ migrations.RunPython(migrate_internal_platforms),
+ ]
diff --git a/apps/assets/migrations/0112_auto_20230404_1631.py b/apps/assets/migrations/0112_auto_20230404_1631.py
new file mode 100644
index 000000000..72285123a
--- /dev/null
+++ b/apps/assets/migrations/0112_auto_20230404_1631.py
@@ -0,0 +1,44 @@
+# Generated by Django 3.2.17 on 2023-04-04 08:31
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('assets', '0111_auto_20230321_1633'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Custom',
+ fields=[
+ ('asset_ptr',
+ models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
+ primary_key=True, serialize=False, to='assets.asset')),
+ ],
+ options={
+ 'verbose_name': 'Custom asset',
+ },
+ bases=('assets.asset',),
+ ),
+ migrations.AddField(
+ model_name='platform',
+ name='custom_fields',
+ field=models.JSONField(default=list, null=True, verbose_name='Custom fields'),
+ ),
+ migrations.AddField(
+ model_name='asset',
+ name='custom_info',
+ field=models.JSONField(default=dict, verbose_name='Custom info'),
+ ),
+ migrations.AddField(
+ model_name='asset',
+ name='gathered_info',
+ field=models.JSONField(blank=True, default=dict, verbose_name='Gathered info'),
+ ),
+ migrations.RemoveField(
+ model_name='asset',
+ name='info',
+ ),
+ ]
diff --git a/apps/assets/migrations/0113_auto_20230411_1814.py b/apps/assets/migrations/0113_auto_20230411_1814.py
new file mode 100644
index 000000000..58fd2fa92
--- /dev/null
+++ b/apps/assets/migrations/0113_auto_20230411_1814.py
@@ -0,0 +1,66 @@
+# Generated by Django 3.2.16 on 2023-04-11 10:14
+
+from django.db import migrations, models
+
+from assets.const import AllTypes
+
+
+def migrate_automation_push_account_params(apps, schema_editor):
+ platform_automation_model = apps.get_model('assets', 'PlatformAutomation')
+ platform_automation_methods = AllTypes.get_automation_methods()
+ methods_id_data_map = {
+ i['id']: None if i['params_serializer'] is None else i['params_serializer']({}).data
+ for i in platform_automation_methods
+ if i['method'] == 'push_account'
+ }
+ automation_objs = []
+ for automation in platform_automation_model.objects.all():
+ push_account_method = automation.push_account_method
+ if not push_account_method:
+ continue
+ value = methods_id_data_map.get(push_account_method)
+ if value is None:
+ continue
+ automation.push_account_params = value
+ automation_objs.append(automation)
+ platform_automation_model.objects.bulk_update(automation_objs, ['push_account_params'])
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('assets', '0112_auto_20230404_1631'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='platformautomation',
+ name='change_secret_params',
+ field=models.JSONField(default=dict, verbose_name='Change secret params'),
+ ),
+ migrations.AddField(
+ model_name='platformautomation',
+ name='gather_accounts_params',
+ field=models.JSONField(default=dict, verbose_name='Gather facts params'),
+ ),
+ migrations.AddField(
+ model_name='platformautomation',
+ name='gather_facts_params',
+ field=models.JSONField(default=dict, verbose_name='Gather facts params'),
+ ),
+ migrations.AddField(
+ model_name='platformautomation',
+ name='ping_params',
+ field=models.JSONField(default=dict, verbose_name='Ping params'),
+ ),
+ migrations.AddField(
+ model_name='platformautomation',
+ name='push_account_params',
+ field=models.JSONField(default=dict, verbose_name='Push account params'),
+ ),
+ migrations.AddField(
+ model_name='platformautomation',
+ name='verify_account_params',
+ field=models.JSONField(default=dict, verbose_name='Verify account params'),
+ ),
+ migrations.RunPython(migrate_automation_push_account_params),
+ ]
diff --git a/apps/assets/migrations/0113_auto_20230411_1917.py b/apps/assets/migrations/0113_auto_20230411_1917.py
new file mode 100644
index 000000000..ca41a30ab
--- /dev/null
+++ b/apps/assets/migrations/0113_auto_20230411_1917.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.2.17 on 2023-04-11 11:17
+
+from django.db import migrations
+
+
+def migrate_device_platform_su_method(apps, schema_editor):
+ platform_model = apps.get_model('assets', 'Platform')
+ device_map = {
+ 'Huawei': 'super',
+ 'Cisco': 'enable',
+ 'H3C': 'super_level',
+ }
+ platforms = platform_model.objects.filter(name__in=device_map.keys())
+ print()
+ for platform in platforms:
+ print("Migrate platform su method: {}".format(platform.name))
+ if platform.name not in device_map:
+ continue
+ platform.su_method = device_map[platform.name]
+ platform.su_enabled = True
+ platform.save(update_fields=['su_method', 'su_enabled'])
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('assets', '0112_auto_20230404_1631'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_device_platform_su_method)
+ ]
diff --git a/apps/assets/migrations/0114_baseautomation_params.py b/apps/assets/migrations/0114_baseautomation_params.py
new file mode 100644
index 000000000..2e6d87f8e
--- /dev/null
+++ b/apps/assets/migrations/0114_baseautomation_params.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.16 on 2023-04-13 10:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assets', '0113_auto_20230411_1814'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='baseautomation',
+ name='params',
+ field=models.JSONField(default=dict, verbose_name='Params'),
+ ),
+ ]
diff --git a/apps/assets/models/asset/__init__.py b/apps/assets/models/asset/__init__.py
index 793df7455..0004bfbb5 100644
--- a/apps/assets/models/asset/__init__.py
+++ b/apps/assets/models/asset/__init__.py
@@ -1,6 +1,7 @@
+from .cloud import *
from .common import *
-from .host import *
+from .custom import *
from .database import *
from .device import *
+from .host import *
from .web import *
-from .cloud import *
diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py
index 3a38699e8..9c81dd0e9 100644
--- a/apps/assets/models/asset/common.py
+++ b/apps/assets/models/asset/common.py
@@ -1,12 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
-
import json
import logging
from collections import defaultdict
from django.db import models
+from django.forms import model_to_dict
from django.utils.translation import ugettext_lazy as _
from assets import const
@@ -94,6 +94,20 @@ class Protocol(models.Model):
def __str__(self):
return '{}/{}'.format(self.name, self.port)
+ @lazyproperty
+ def asset_platform_protocol(self):
+ protocols = self.asset.platform.protocols.values('name', 'public', 'setting')
+ protocols = list(filter(lambda p: p['name'] == self.name, protocols))
+ return protocols[0] if len(protocols) > 0 else {}
+
+ @property
+ def setting(self):
+ return self.asset_platform_protocol.get('setting', {})
+
+ @property
+ def public(self):
+ return self.asset_platform_protocol.get('public', True)
+
class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
Category = const.Category
@@ -108,7 +122,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
- info = models.JSONField(verbose_name=_('Info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
+ gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
+ custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
objects = AssetManager.from_queryset(AssetQuerySet)()
@@ -148,20 +163,27 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return self.get_spec_values(instance, spec_fields)
@lazyproperty
- def auto_info(self):
+ def info(self):
+ info = {}
+ info.update(self.gathered_info or {})
+ info.update(self.custom_info or {})
+ info.update(self.spec_info or {})
+ return info
+
+ @lazyproperty
+ def auto_config(self):
platform = self.platform
automation = self.platform.automation
- return {
+ auto_config = {
'su_enabled': platform.su_enabled,
- 'ping_enabled': automation.ping_enabled,
'domain_enabled': platform.domain_enabled,
- 'ansible_enabled': automation.ansible_enabled,
- 'push_account_enabled': automation.push_account_enabled,
- 'gather_facts_enabled': automation.gather_facts_enabled,
- 'change_secret_enabled': automation.change_secret_enabled,
- 'verify_account_enabled': automation.verify_account_enabled,
- 'gather_accounts_enabled': automation.gather_accounts_enabled,
+ 'ansible_enabled': False
}
+ if not automation:
+ return auto_config
+
+ auto_config.update(model_to_dict(automation))
+ return auto_config
def get_target_ip(self):
return self.address
@@ -191,25 +213,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
names.append(n.name + ':' + n.value)
return names
- @lazyproperty
- def primary_protocol(self):
- from assets.const.types import AllTypes
- primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type)
- protocol = self.protocols.filter(name=primary_protocol_name).first()
- return protocol
-
- @lazyproperty
- def protocol(self):
- if not self.primary_protocol:
- return 'none'
- return self.primary_protocol.name
-
- @lazyproperty
- def port(self):
- if not self.primary_protocol:
- return 0
- return self.primary_protocol.port
-
@lazyproperty
def type(self):
return self.platform.type
@@ -275,6 +278,22 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
tree_node = TreeNode(**data)
return tree_node
+ @staticmethod
+ def get_secret_type_assets(asset_ids, secret_type):
+ assets = Asset.objects.filter(id__in=asset_ids)
+ asset_protocol = assets.prefetch_related('protocols').values_list('id', 'protocols__name')
+ protocol_secret_types_map = const.Protocol.protocol_secret_types()
+ asset_secret_types_mapp = defaultdict(set)
+
+ for asset_id, protocol in asset_protocol:
+ secret_types = set(protocol_secret_types_map.get(protocol, []))
+ asset_secret_types_mapp[asset_id].update(secret_types)
+
+ return [
+ asset for asset in assets
+ if secret_type in asset_secret_types_mapp.get(asset.id, [])
+ ]
+
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Asset")
diff --git a/apps/assets/models/asset/custom.py b/apps/assets/models/asset/custom.py
new file mode 100644
index 000000000..c3e4263ba
--- /dev/null
+++ b/apps/assets/models/asset/custom.py
@@ -0,0 +1,8 @@
+from django.utils.translation import gettext_lazy as _
+
+from .common import Asset
+
+
+class Custom(Asset):
+ class Meta:
+ verbose_name = _("Custom asset")
diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py
index 9ddeb5bf2..e41092fbf 100644
--- a/apps/assets/models/automations/base.py
+++ b/apps/assets/models/automations/base.py
@@ -19,6 +19,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
type = models.CharField(max_length=16, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
+ params = models.JSONField(default=dict, verbose_name=_("Params"))
def __str__(self):
return self.name + '@' + str(self.created_by)
diff --git a/apps/assets/models/gateway.py b/apps/assets/models/gateway.py
index d9d4d891b..0fc6626df 100644
--- a/apps/assets/models/gateway.py
+++ b/apps/assets/models/gateway.py
@@ -2,11 +2,10 @@
#
from django.utils.translation import ugettext_lazy as _
-from orgs.mixins.models import OrgManager
-from assets.models.platform import Platform
from assets.const import GATEWAY_NAME
+from assets.models.platform import Platform
from common.utils import get_logger, lazyproperty
-
+from orgs.mixins.models import OrgManager
from .asset.host import Host
logger = get_logger(__file__)
@@ -57,6 +56,14 @@ class Gateway(Host):
account = self.select_account
return account.password if account else None
+ @lazyproperty
+ def port(self):
+ protocol = self.protocols.filter(name='ssh').first()
+ if protocol:
+ return protocol.port
+ else:
+ return '22'
+
@lazyproperty
def private_key(self):
account = self.select_account
diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py
index 488525ccd..4927ce793 100644
--- a/apps/assets/models/platform.py
+++ b/apps/assets/models/platform.py
@@ -10,56 +10,68 @@ __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
class PlatformProtocol(models.Model):
- SETTING_ATTRS = {
- 'console': False,
- 'security': 'any,tls,rdp',
- 'sftp_enabled': True,
- 'sftp_home': '/tmp'
- }
- default = models.BooleanField(default=False, verbose_name=_('Default'))
- required = models.BooleanField(default=False, verbose_name=_('Required'))
name = models.CharField(max_length=32, verbose_name=_('Name'))
port = models.IntegerField(verbose_name=_('Port'))
+ primary = models.BooleanField(default=False, verbose_name=_('Primary'))
+ required = models.BooleanField(default=False, verbose_name=_('Required'))
+ default = models.BooleanField(default=False, verbose_name=_('Default'))
+ public = models.BooleanField(default=True, verbose_name=_('Public'))
setting = models.JSONField(verbose_name=_('Setting'), default=dict)
platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols')
def __str__(self):
return '{}/{}'.format(self.name, self.port)
- @property
- def primary(self):
- primary_protocol_name = AllTypes.get_primary_protocol_name(
- self.platform.category, self.platform.type
- )
- return self.name == primary_protocol_name
-
@property
def secret_types(self):
- return Protocol.settings().get(self.name, {}).get('secret_types')
+ return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
+
+ def set_public(self):
+ private_protocol_set = ('winrm',)
+ self.public = self.name not in private_protocol_set
+
+ def save(self, **kwargs):
+ self.set_public()
+ return super().save(**kwargs)
class PlatformAutomation(models.Model):
ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled"))
ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config"))
+
ping_enabled = models.BooleanField(default=False, verbose_name=_("Ping enabled"))
ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method"))
+ ping_params = models.JSONField(default=dict, verbose_name=_("Ping params"))
+
gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
- gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
+ gather_facts_method = models.TextField(
+ max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
+ )
+ gather_facts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
+
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled"))
change_secret_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Change secret method")
)
+ change_secret_params = models.JSONField(default=dict, verbose_name=_("Change secret params"))
+
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
push_account_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Push account method")
)
+ push_account_params = models.JSONField(default=dict, verbose_name=_("Push account params"))
+
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
verify_account_method = models.TextField(
- max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
+ max_length=32, blank=True, null=True, verbose_name=_("Verify account method")
+ )
+ verify_account_params = models.JSONField(default=dict, verbose_name=_("Verify account params"))
+
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
gather_accounts_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
)
+ gather_accounts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
class Platform(JMSBaseModel):
@@ -80,14 +92,18 @@ class Platform(JMSBaseModel):
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
# 资产有关的
charset = models.CharField(
- default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
+ default=CharsetChoices.utf8, choices=CharsetChoices.choices,
+ max_length=8, verbose_name=_("Charset")
)
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
# 账号有关的
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
- automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
- blank=True, null=True, verbose_name=_("Automation"))
+ automation = models.OneToOneField(
+ PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
+ blank=True, null=True, verbose_name=_("Automation")
+ )
+ custom_fields = models.JSONField(null=True, default=list, verbose_name=_("Custom fields"))
@property
def type_constraints(self):
@@ -100,11 +116,6 @@ class Platform(JMSBaseModel):
)
return linux.id
- @property
- def primary_protocol(self):
- primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type)
- return self.protocols.filter(name=primary_protocol_name).first()
-
def __str__(self):
return self.name
diff --git a/apps/assets/notifications.py b/apps/assets/notifications.py
new file mode 100644
index 000000000..14e84768e
--- /dev/null
+++ b/apps/assets/notifications.py
@@ -0,0 +1,25 @@
+from django.utils.translation import ugettext as _
+
+from notifications.notifications import UserMessage
+
+
+class BulkUpdatePlatformSkipAssetUserMsg(UserMessage):
+ def __init__(self, user, assets):
+ super().__init__(user)
+ self.assets = assets
+
+ def get_html_msg(self) -> dict:
+ subject = _("Batch update platform in assets, skipping assets that do not meet platform type")
+ message = f'{"".join([f"- {asset}
" for asset in self.assets])}
'
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ @classmethod
+ def gen_test_msg(cls):
+ from users.models import User
+ from assets.models import Asset
+ user = User.objects.first()
+ assets = Asset.objects.all()[:10]
+ return cls(user, assets)
\ No newline at end of file
diff --git a/apps/assets/serializers/asset/__init__.py b/apps/assets/serializers/asset/__init__.py
index 12f1eb66c..8e3e14cf3 100644
--- a/apps/assets/serializers/asset/__init__.py
+++ b/apps/assets/serializers/asset/__init__.py
@@ -1,6 +1,8 @@
+# No pass
+from .cloud import *
from .common import *
-from .host import *
+from .custom import *
from .database import *
from .device import *
-from .cloud import *
+from .host import *
from .web import *
diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py
index d7dc7b175..2a50d3419 100644
--- a/apps/assets/serializers/asset/common.py
+++ b/apps/assets/serializers/asset/common.py
@@ -1,15 +1,18 @@
# -*- coding: utf-8 -*-
#
+import re
+
from django.db.models import F
from django.db.transaction import atomic
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from accounts.models import Account
-from accounts.serializers import AccountSerializerCreateValidateMixin
-from accounts.serializers import AuthValidateMixin
-from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
+from accounts.serializers import AccountSerializer
+from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \
+ MethodSerializer
+from common.serializers.dynamic import create_serializer_class
from common.serializers.fields import LabeledChoiceField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes
@@ -19,9 +22,11 @@ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer',
- 'AccountSecretSerializer', 'SpecSerializer'
+ 'AccountSecretSerializer', 'AssetProtocolsPermsSerializer'
]
+uuid_pattern = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
+
class AssetProtocolsSerializer(serializers.ModelSerializer):
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1)
@@ -38,6 +43,11 @@ class AssetProtocolsSerializer(serializers.ModelSerializer):
fields = ['name', 'port']
+class AssetProtocolsPermsSerializer(AssetProtocolsSerializer):
+ class Meta(AssetProtocolsSerializer.Meta):
+ fields = AssetProtocolsSerializer.Meta.fields + ['public', 'setting']
+
+
class AssetLabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
@@ -59,45 +69,39 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
}
-class AssetAccountSerializer(
- AuthValidateMixin,
- AccountSerializerCreateValidateMixin,
- CommonModelSerializer
-):
+class AssetAccountSerializer(AccountSerializer):
add_org_fields = False
- push_now = serializers.BooleanField(
- default=False, label=_("Push now"), write_only=True
- )
- template = serializers.BooleanField(
- default=False, label=_("Template"), write_only=True
- )
- name = serializers.CharField(max_length=128, required=True, label=_("Name"))
+ asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True)
+ clone_id: str
- class Meta:
- model = Account
- fields_mini = [
- 'id', 'name', 'username', 'privileged',
- 'is_active', 'version', 'secret_type',
+ def to_internal_value(self, data):
+ clone_id = data.pop('id', None)
+ ret = super().to_internal_value(data)
+ self.clone_id = clone_id
+ return ret
+
+ def set_secret(self, attrs):
+ _id = self.clone_id
+ if not _id:
+ return attrs
+
+ account = Account.objects.get(id=_id)
+ attrs['secret'] = account.secret
+ return attrs
+
+ def validate(self, attrs):
+ attrs = super().validate(attrs)
+ return self.set_secret(attrs)
+
+ class Meta(AccountSerializer.Meta):
+ fields = [
+ f for f in AccountSerializer.Meta.fields
+ if f not in ['spec_info']
]
- fields_write_only = [
- 'secret', 'passphrase', 'push_now', 'template'
- ]
- fields = fields_mini + fields_write_only
extra_kwargs = {
- 'secret': {'write_only': True},
+ **AccountSerializer.Meta.extra_kwargs,
}
- def validate_push_now(self, value):
- request = self.context['request']
- if not request.user.has_perms('accounts.push_account'):
- return False
- return value
-
- def validate_name(self, value):
- if not value:
- value = self.initial_data.get('username')
- return value
-
class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
class Meta:
@@ -110,44 +114,32 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
}
-class SpecSerializer(serializers.Serializer):
- # 数据库
- db_name = serializers.CharField(label=_("Database"), max_length=128, required=False)
- use_ssl = serializers.BooleanField(label=_("Use SSL"), required=False)
- allow_invalid_cert = serializers.BooleanField(label=_("Allow invalid cert"), required=False)
- # Web
- autofill = serializers.CharField(label=_("Auto fill"), required=False)
- username_selector = serializers.CharField(label=_("Username selector"), required=False)
- password_selector = serializers.CharField(label=_("Password selector"), required=False)
- submit_selector = serializers.CharField(label=_("Submit selector"), required=False)
- script = serializers.JSONField(label=_("Script"), required=False)
-
-
class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
- nodes_display = serializers.ListField(read_only=True, label=_("Node path"))
+ nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
+ custom_info = MethodSerializer(label=_('Custom info'))
class Meta:
model = Asset
fields_mini = ['id', 'name', 'address']
- fields_small = fields_mini + ['is_active', 'comment']
+ fields_small = fields_mini + ['custom_info', 'is_active', 'comment']
fields_fk = ['domain', 'platform']
fields_m2m = [
'nodes', 'labels', 'protocols',
- 'nodes_display', 'accounts'
+ 'nodes_display', 'accounts',
]
read_only_fields = [
- 'category', 'type', 'connectivity', 'auto_info',
+ 'category', 'type', 'connectivity', 'auto_config',
'date_verified', 'created_by', 'date_created',
]
fields = fields_small + fields_fk + fields_m2m + read_only_fields
- fields_unexport = ['auto_info']
+ fields_unexport = ['auto_config']
extra_kwargs = {
- 'auto_info': {'label': _('Auto info')},
+ 'auto_config': {'label': _('Auto info')},
'name': {'label': _("Name")},
'address': {'label': _('Address')},
'nodes_display': {'label': _('Node path')},
@@ -197,6 +189,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
.annotate(type=F("platform__type"))
return queryset
+ def get_custom_info_serializer(self):
+ request = self.context.get('request')
+ default_field = serializers.DictField(required=False, label=_('Custom info'))
+
+ if not request:
+ return default_field
+
+ if self.instance and isinstance(self.instance, list):
+ return default_field
+
+ if not self.instance and uuid_pattern.findall(request.path):
+ pk = uuid_pattern.findall(request.path)[0]
+ self.instance = Asset.objects.filter(id=pk).first()
+
+ platform = None
+ if self.instance:
+ platform = self.instance.platform
+ elif request.query_params.get('platform'):
+ platform_id = request.query_params.get('platform')
+ platform_id = int(platform_id) if platform_id.isdigit() else 0
+ platform = Platform.objects.filter(id=platform_id).first()
+
+ if not platform:
+ return default_field
+ custom_fields = platform.custom_fields
+ if not custom_fields:
+ return default_field
+ name = platform.name.title() + 'CustomSerializer'
+ return create_serializer_class(name, custom_fields)()
+
@staticmethod
def perform_nodes_display_create(instance, nodes_display):
if not nodes_display:
@@ -276,8 +298,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
if not accounts_data:
return
for data in accounts_data:
- data['asset'] = asset
- AssetAccountSerializer().create(data)
+ data['asset'] = asset.id
+
+ s = AssetAccountSerializer(data=accounts_data, many=True)
+ s.is_valid(raise_exception=True)
+ s.save()
@atomic
def create(self, validated_data):
@@ -300,16 +325,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
class DetailMixin(serializers.Serializer):
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
- spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
- auto_info = serializers.DictField(read_only=True, label=_('Auto info'))
+ spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
+ gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
+ auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
+
+ def get_instance(self):
+ request = self.context.get('request')
+ if not self.instance and uuid_pattern.findall(request.path):
+ pk = uuid_pattern.findall(request.path)[0]
+ self.instance = Asset.objects.filter(id=pk).first()
+ return self.instance
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
names.extend([
- 'accounts', 'info', 'spec_info', 'auto_info'
+ 'accounts', 'gathered_info', 'spec_info',
+ 'auto_config',
])
return names
+ def get_category(self):
+ request = self.context.get('request')
+ if request.query_params.get('category'):
+ category = request.query_params.get('category')
+ else:
+ instance = self.get_instance()
+ category = instance.category
+ return category
+
+ def get_gathered_info_serializer(self):
+ category = self.get_category()
+ from .info.gathered import category_gathered_serializer_map
+ serializer_cls = category_gathered_serializer_map.get(category, serializers.DictField)
+ return serializer_cls()
+
+ def get_spec_info_serializer(self):
+ category = self.get_category()
+ from .info.spec import category_spec_serializer_map
+ serializer_cls = category_spec_serializer_map.get(category, serializers.DictField)
+ return serializer_cls()
+
class AssetDetailSerializer(DetailMixin, AssetSerializer):
pass
diff --git a/apps/assets/serializers/asset/custom.py b/apps/assets/serializers/asset/custom.py
new file mode 100644
index 000000000..d88024218
--- /dev/null
+++ b/apps/assets/serializers/asset/custom.py
@@ -0,0 +1,9 @@
+from assets.models import Custom
+from .common import AssetSerializer
+
+__all__ = ['CustomSerializer']
+
+
+class CustomSerializer(AssetSerializer):
+ class Meta(AssetSerializer.Meta):
+ model = Custom
diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py
index 9f5e97dba..da6c9574a 100644
--- a/apps/assets/serializers/asset/database.py
+++ b/apps/assets/serializers/asset/database.py
@@ -1,9 +1,9 @@
-from rest_framework.serializers import ValidationError
from django.utils.translation import ugettext_lazy as _
+from rest_framework.serializers import ValidationError
from assets.models import Database
+from assets.serializers.gateway import GatewayWithAccountSecretSerializer
from .common import AssetSerializer
-from ..gateway import GatewayWithAccountSecretSerializer
__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer']
diff --git a/apps/assets/serializers/asset/host.py b/apps/assets/serializers/asset/host.py
index 0f6bd8f5d..c3575f43b 100644
--- a/apps/assets/serializers/asset/host.py
+++ b/apps/assets/serializers/asset/host.py
@@ -1,34 +1,18 @@
from django.utils.translation import gettext_lazy as _
-from rest_framework import serializers
from assets.models import Host
from .common import AssetSerializer
+from .info.gathered import HostGatheredInfoSerializer
-__all__ = ['HostInfoSerializer', 'HostSerializer']
-
-
-class HostInfoSerializer(serializers.Serializer):
- vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
- model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
- sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
- cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
- cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
- cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
- cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
- memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
- disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
-
- distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
- distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
- arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
+__all__ = ['HostSerializer']
class HostSerializer(AssetSerializer):
- info = HostInfoSerializer(required=False, label=_('Info'))
+ gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info"))
class Meta(AssetSerializer.Meta):
model = Host
- fields = AssetSerializer.Meta.fields + ['info']
+ fields = AssetSerializer.Meta.fields + ['gathered_info']
extra_kwargs = {
**AssetSerializer.Meta.extra_kwargs,
'address': {
diff --git a/apps/assets/serializers/asset/info/gathered.py b/apps/assets/serializers/asset/info/gathered.py
new file mode 100644
index 000000000..db0f22e65
--- /dev/null
+++ b/apps/assets/serializers/asset/info/gathered.py
@@ -0,0 +1,23 @@
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+
+class HostGatheredInfoSerializer(serializers.Serializer):
+ vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
+ model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
+ sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
+ cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
+ cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
+ cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
+ cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
+ memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
+ disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
+
+ distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
+ distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
+ arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
+
+
+category_gathered_serializer_map = {
+ 'host': HostGatheredInfoSerializer,
+}
diff --git a/apps/assets/serializers/asset/info/spec.py b/apps/assets/serializers/asset/info/spec.py
new file mode 100644
index 000000000..5d3de54c3
--- /dev/null
+++ b/apps/assets/serializers/asset/info/spec.py
@@ -0,0 +1,24 @@
+from rest_framework import serializers
+
+from assets.models import Database, Web
+
+
+class DatabaseSpecSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Database
+ fields = ['db_name', 'use_ssl', 'allow_invalid_cert']
+
+
+class WebSpecSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Web
+ fields = [
+ 'autofill', 'username_selector', 'password_selector',
+ 'submit_selector', 'script'
+ ]
+
+
+category_spec_serializer_map = {
+ 'database': DatabaseSpecSerializer,
+ 'web': WebSpecSerializer,
+}
diff --git a/apps/assets/serializers/asset/web.py b/apps/assets/serializers/asset/web.py
index aa35e28e0..c6e05f6c0 100644
--- a/apps/assets/serializers/asset/web.py
+++ b/apps/assets/serializers/asset/web.py
@@ -24,6 +24,6 @@ class WebSerializer(AssetSerializer):
'default': 'name=password'
},
'submit_selector': {
- 'default': 'id=longin_button',
+ 'default': 'id=login_button',
},
}
diff --git a/apps/assets/serializers/gateway.py b/apps/assets/serializers/gateway.py
index 78d8afda5..259bc13f8 100644
--- a/apps/assets/serializers/gateway.py
+++ b/apps/assets/serializers/gateway.py
@@ -3,8 +3,8 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
-from .asset import HostSerializer
from .asset.common import AccountSecretSerializer
+from .asset.host import HostSerializer
from ..models import Gateway, Asset
__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer']
diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py
index 15b4e7cca..c542cc20a 100644
--- a/apps/assets/serializers/platform.py
+++ b/apps/assets/serializers/platform.py
@@ -4,6 +4,7 @@ from rest_framework import serializers
from assets.const.web import FillType
from common.serializers import WritableNestedModelSerializer
from common.serializers.fields import LabeledChoiceField
+from common.utils import lazyproperty
from ..const import Category, AllTypes
from ..models import Platform, PlatformProtocol, PlatformAutomation
@@ -37,10 +38,12 @@ class ProtocolSettingSerializer(serializers.Serializer):
default="", allow_blank=True, label=_("Submit selector")
)
script = serializers.JSONField(default=list, label=_("Script"))
-
# Redis
auth_username = serializers.BooleanField(default=False, label=_("Auth with username"))
+ # WinRM
+ use_ssl = serializers.BooleanField(default=False, label=_("Use SSL"))
+
class PlatformAutomationSerializer(serializers.ModelSerializer):
class Meta:
@@ -48,12 +51,12 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
fields = [
"id",
"ansible_enabled", "ansible_config",
- "ping_enabled", "ping_method",
- "push_account_enabled", "push_account_method",
- "gather_facts_enabled", "gather_facts_method",
- "change_secret_enabled", "change_secret_method",
- "verify_account_enabled", "verify_account_method",
- "gather_accounts_enabled", "gather_accounts_method",
+ "ping_enabled", "ping_method", "ping_params",
+ "push_account_enabled", "push_account_method", "push_account_params",
+ "gather_facts_enabled", "gather_facts_method", "gather_facts_params",
+ "change_secret_enabled", "change_secret_method", "change_secret_params",
+ "verify_account_enabled", "verify_account_method", "verify_account_params",
+ "gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
]
extra_kwargs = {
# 启用资产探测
@@ -72,33 +75,53 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
}
-class PlatformProtocolsSerializer(serializers.ModelSerializer):
+class PlatformProtocolSerializer(serializers.ModelSerializer):
setting = ProtocolSettingSerializer(required=False, allow_null=True)
- primary = serializers.BooleanField(read_only=True, label=_("Primary"))
class Meta:
model = PlatformProtocol
fields = [
"id", "name", "port", "primary",
- "default", "required", "secret_types",
- "setting",
+ "required", "default",
+ "secret_types", "setting",
]
+class PlatformCustomField(serializers.Serializer):
+ TYPE_CHOICES = [
+ ("str", "str"),
+ ("text", "text"),
+ ("int", "int"),
+ ("bool", "bool"),
+ ("choice", "choice"),
+ ("list", "list"),
+ ]
+ name = serializers.CharField(label=_("Name"), max_length=128)
+ label = serializers.CharField(label=_("Label"), max_length=128)
+ type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str')
+ default = serializers.CharField(default="", allow_blank=True, label=_("Default"), max_length=1024)
+ help_text = serializers.CharField(default="", allow_blank=True, label=_("Help text"), max_length=1024)
+ choices = serializers.ListField(default=list, label=_("Choices"), required=False)
+
+
class PlatformSerializer(WritableNestedModelSerializer):
- charset = LabeledChoiceField(
- choices=Platform.CharsetChoices.choices, label=_("Charset")
- )
+ SU_METHOD_CHOICES = [
+ ("sudo", "sudo su -"),
+ ("su", "su - "),
+ ("enable", "enable"),
+ ("super", "super 15"),
+ ("super_level", "super level 15")
+ ]
+ charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
- protocols = PlatformProtocolsSerializer(
- label=_("Protocols"), many=True, required=False
- )
- automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
+ protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
+ automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField(
- choices=[("sudo", "sudo su -"), ("su", "su - ")],
- label=_("Su method"), required=False, default="sudo", allow_null=True
+ choices=SU_METHOD_CHOICES, label=_("Su method"),
+ required=False, default="sudo", allow_null=True
)
+ custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
class Meta:
model = Platform
@@ -106,19 +129,54 @@ class PlatformSerializer(WritableNestedModelSerializer):
fields_small = fields_mini + [
"category", "type", "charset",
]
- fields_other = [
- 'date_created', 'date_updated', 'created_by', 'updated_by',
+ read_only_fields = [
+ 'internal', 'date_created', 'date_updated',
+ 'created_by', 'updated_by'
]
fields = fields_small + [
"protocols", "domain_enabled", "su_enabled",
- "su_method", "automation", "comment",
- ] + fields_other
+ "su_method", "automation", "comment", "custom_fields",
+ ] + read_only_fields
extra_kwargs = {
"su_enabled": {"label": _('Su enabled')},
"domain_enabled": {"label": _('Domain enabled')},
"domain_default": {"label": _('Default Domain')},
}
+ @property
+ def platform_category_type(self):
+ if self.instance:
+ return self.instance.category, self.instance.type
+ if self.initial_data:
+ return self.initial_data.get('category'), self.initial_data.get('type')
+ raise serializers.ValidationError({'type': _("type is required")})
+
+ def add_type_choices(self, name, label):
+ tp = self.fields['type']
+ tp.choices[name] = label
+ tp.choice_mapper[name] = label
+ tp.choice_strings_to_values[name] = label
+
+ @lazyproperty
+ def constraints(self):
+ category, tp = self.platform_category_type
+ constraints = AllTypes.get_constraints(category, tp)
+ return constraints
+
+ def validate(self, attrs):
+ domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False)
+ su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False)
+ automation = attrs.get('automation', {})
+ automation['ansible_enabled'] = automation.get('ansible_enabled', False) \
+ and self.constraints.get('ansible_enabled', False)
+ attrs.update({
+ 'domain_enabled': domain_enabled,
+ 'su_enabled': su_enabled,
+ 'automation': automation,
+ })
+ self.initial_data['automation'] = automation
+ return attrs
+
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related(
@@ -126,6 +184,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
)
return queryset
+ def validate_protocols(self, protocols):
+ if not protocols:
+ raise serializers.ValidationError(_("Protocols is required"))
+ primary = [p for p in protocols if p.get('primary')]
+ if not primary:
+ protocols[0]['primary'] = True
+ # 这里不设置不行,write_nested 不使用 validated 中的
+ self.initial_data['protocols'] = protocols
+ return protocols
+
class PlatformOpsMethodSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True)
diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py
index 107be4422..9598bec5d 100644
--- a/apps/assets/signal_handlers/asset.py
+++ b/apps/assets/signal_handlers/asset.py
@@ -66,11 +66,11 @@ def on_asset_create(sender, instance=None, created=False, **kwargs):
ensure_asset_has_node(assets=(instance,))
# 获取资产硬件信息
- auto_info = instance.auto_info
- if auto_info.get('ping_enabled'):
+ auto_config = instance.auto_config
+ if auto_config.get('ping_enabled'):
logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name))
test_assets_connectivity_handler(assets=(instance,))
- if auto_info.get('gather_facts_enabled'):
+ if auto_config.get('gather_facts_enabled'):
logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name))
gather_assets_facts_handler(assets=(instance,))
diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py
index a9a385eb9..6b5f469d0 100644
--- a/apps/assets/urls/api_urls.py
+++ b/apps/assets/urls/api_urls.py
@@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device')
router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'webs', api.WebViewSet, 'web')
router.register(r'clouds', api.CloudViewSet, 'cloud')
+router.register(r'customs', api.CustomViewSet, 'custom')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node')
@@ -45,6 +46,7 @@ urlpatterns = [
path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
+ path('platform-automation-methods/', api.PlatformAutomationMethodsApi.as_view(), name='platform-automation-methods'),
]
urlpatterns += router.urls
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index dd225073e..5b5dcd700 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -162,7 +162,8 @@ class RDPFileClientProtocolURLMixin:
def get_smart_endpoint(self, protocol, asset=None):
target_ip = asset.get_target_ip() if asset else ''
endpoint = EndpointRule.match_endpoint(
- target_instance=asset, target_ip=target_ip, protocol=protocol, request=self.request
+ target_instance=asset, target_ip=target_ip,
+ protocol=protocol, request=self.request
)
return endpoint
diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py
index 6a69a7e37..63049170b 100644
--- a/apps/authentication/models/connection_token.py
+++ b/apps/authentication/models/connection_token.py
@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import PermissionDenied
from assets.const import Protocol
+from assets.const.host import GATEWAY_NAME
from common.db.fields import EncryptTextField
from common.exceptions import JMSException
from common.utils import lazyproperty, pretty_string, bulk_get
@@ -231,12 +232,14 @@ class ConnectionToken(JMSOrgBaseModel):
def domain(self):
if not self.asset.platform.domain_enabled:
return
- domain = self.asset.domain if self.asset else None
+ if self.asset.platform.name == GATEWAY_NAME:
+ return
+ domain = self.asset.domain if self.asset.domain else None
return domain
@lazyproperty
def gateway(self):
- if not self.asset:
+ if not self.asset or not self.domain:
return
return self.asset.gateway
diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py
index 003d8239e..b96a99171 100644
--- a/apps/authentication/serializers/connect_token_secret.py
+++ b/apps/authentication/serializers/connect_token_secret.py
@@ -5,7 +5,8 @@ from accounts.const import SecretType
from accounts.models import Account
from acls.models import CommandGroup, CommandFilterACL
from assets.models import Asset, Platform, Gateway, Domain
-from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
+from assets.serializers.asset import AssetProtocolsSerializer
+from assets.serializers.platform import PlatformSerializer
from common.serializers.fields import LabeledChoiceField
from common.serializers.fields import ObjectRelatedField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
@@ -26,18 +27,17 @@ class _ConnectionTokenUserSerializer(serializers.ModelSerializer):
class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
+ info = serializers.DictField()
class Meta:
model = Asset
fields = [
- 'id', 'name', 'address', 'protocols',
- 'category', 'type', 'org_id', 'spec_info',
- 'secret_info',
+ 'id', 'name', 'address', 'protocols', 'category',
+ 'type', 'org_id', 'info', 'secret_info', 'spec_info'
]
class _SimpleAccountSerializer(serializers.ModelSerializer):
- """ Account """
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta:
@@ -46,20 +46,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
- """ Account """
su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta:
model = Account
fields = [
- 'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged'
+ 'id', 'name', 'username', 'secret_type',
+ 'secret', 'su_from', 'privileged'
]
class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
- """ Gateway """
-
account = _SimpleAccountSerializer(
required=False, source='select_account', read_only=True
)
@@ -85,7 +83,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterACL
fields = [
- 'id', 'name', 'command_groups', 'action', 'reviewers', 'priority', 'is_active'
+ 'id', 'name', 'command_groups', 'action',
+ 'reviewers', 'priority', 'is_active'
]
@@ -136,8 +135,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'protocol',
'domain', 'gateway', 'actions', 'expire_at',
- 'from_ticket',
- 'expire_now', 'connect_method',
+ 'from_ticket', 'expire_now', 'connect_method',
]
extra_kwargs = {
'value': {'read_only': True},
diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py
index 7f3d4b055..9ba77ca72 100644
--- a/apps/common/drf/parsers/base.py
+++ b/apps/common/drf/parsers/base.py
@@ -111,7 +111,7 @@ class BaseFileParser(BaseParser):
return {'pk': obj_id, 'name': obj_name}
def parse_value(self, field, value):
- if value is '-':
+ if value == '-' and field and field.allow_null:
return None
elif hasattr(field, 'to_file_internal_value'):
value = field.to_file_internal_value(value)
diff --git a/apps/common/serializers/__init__.py b/apps/common/serializers/__init__.py
index 7ecadafc7..6d79241fc 100644
--- a/apps/common/serializers/__init__.py
+++ b/apps/common/serializers/__init__.py
@@ -1,2 +1,3 @@
from .common import *
+from .dynamic import *
from .mixin import *
diff --git a/apps/common/serializers/dynamic.py b/apps/common/serializers/dynamic.py
new file mode 100644
index 000000000..9ab26b9bb
--- /dev/null
+++ b/apps/common/serializers/dynamic.py
@@ -0,0 +1,55 @@
+from rest_framework import serializers
+
+example_info = [
+ {"name": "name", "label": "姓名", "required": False, "default": "广州老广", "type": "str"},
+ {"name": "age", "label": "年龄", "required": False, "default": 18, "type": "int"},
+]
+
+type_field_map = {
+ "str": serializers.CharField,
+ "int": serializers.IntegerField,
+ "bool": serializers.BooleanField,
+ "text": serializers.CharField,
+ "choice": serializers.ChoiceField,
+ "list": serializers.ListField,
+}
+
+
+def set_default_if_need(data, i):
+ field_name = data.pop('name', 'Attr{}'.format(i + 1))
+ data['name'] = field_name
+
+ if not data.get('label'):
+ data['label'] = field_name
+ return data
+
+
+def set_default_by_type(tp, data, field_info):
+ if tp == 'str':
+ data['max_length'] = 4096
+ elif tp == 'choice':
+ choices = field_info.pop('choices', [])
+ if isinstance(choices, str):
+ choices = choices.split(',')
+ choices = [
+ (c, c.title()) if not isinstance(c, (tuple, list)) else c
+ for c in choices
+ ]
+ data['choices'] = choices
+ return data
+
+
+def create_serializer_class(serializer_name, fields_info):
+ serializer_fields = {}
+ fields_name = ['name', 'label', 'default', 'type', 'help_text']
+
+ for i, field_info in enumerate(fields_info):
+ data = {k: field_info.get(k) for k in fields_name}
+ field_type = data.pop('type', 'str')
+ data = set_default_by_type(field_type, data, field_info)
+ data = set_default_if_need(data, i)
+ field_name = data.pop('name')
+ field_class = type_field_map.get(field_type, serializers.CharField)
+ serializer_fields[field_name] = field_class(**data)
+
+ return type(serializer_name, (serializers.Serializer,), serializer_fields)
diff --git a/apps/common/serializers/fields.py b/apps/common/serializers/fields.py
index 84d4d5927..514a96fd0 100644
--- a/apps/common/serializers/fields.py
+++ b/apps/common/serializers/fields.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
+import phonenumbers
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -17,6 +18,7 @@ __all__ = [
"BitChoicesField",
"TreeChoicesField",
"LabeledMultipleChoiceField",
+ "PhoneField",
]
@@ -201,3 +203,11 @@ class BitChoicesField(TreeChoicesField):
value = self.to_internal_value(data)
self.run_validators(value)
return value
+
+
+class PhoneField(serializers.CharField):
+ def to_representation(self, value):
+ if value:
+ phone = phonenumbers.parse(value, 'CN')
+ value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
+ return value
diff --git a/apps/common/validators.py b/apps/common/validators.py
index 4be90d855..5bb3e4bdb 100644
--- a/apps/common/validators.py
+++ b/apps/common/validators.py
@@ -2,12 +2,15 @@
#
import re
+import phonenumbers
+
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from rest_framework.validators import (
UniqueTogetherValidator, ValidationError
)
from rest_framework import serializers
+from phonenumbers.phonenumberutil import NumberParseException
from common.utils.strings import no_special_chars
@@ -42,9 +45,14 @@ class NoSpecialChars:
class PhoneValidator:
- pattern = re.compile(r"^1[3456789]\d{9}$")
message = _('The mobile phone number format is incorrect')
def __call__(self, value):
- if not self.pattern.match(value):
+ try:
+ phone = phonenumbers.parse(value, 'CN')
+ valid = phonenumbers.is_valid_number(phone)
+ except NumberParseException:
+ valid = False
+
+ if not valid:
raise serializers.ValidationError(self.message)
diff --git a/apps/common/views/mixins.py b/apps/common/views/mixins.py
index cb2e402c8..562e9ca81 100644
--- a/apps/common/views/mixins.py
+++ b/apps/common/views/mixins.py
@@ -1,15 +1,17 @@
# -*- coding: utf-8 -*-
#
from django.utils import translation
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_noop
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http.response import JsonResponse
from rest_framework import permissions
from rest_framework.request import Request
from common.exceptions import UserConfirmRequired
+from common.utils import i18n_fmt
from audits.handler import create_or_update_operate_log
-from audits.const import ActionChoices
+from audits.const import ActionChoices, ActivityChoices
+from audits.models import ActivityLog
__all__ = [
"PermissionsMixin",
@@ -49,38 +51,63 @@ class RecordViewLogMixin:
ACTION = ActionChoices.view
@staticmethod
- def get_resource_display(request):
+ def _filter_params(params):
+ new_params = {}
+ need_pop_params = ('format', 'order')
+ for key, value in params.items():
+ if key in need_pop_params:
+ continue
+ if isinstance(value, list):
+ value = list(filter(None, value))
+ if value:
+ new_params[key] = value
+ return new_params
+
+ def get_resource_display(self, request):
query_params = dict(request.query_params)
- if query_params.get("format"):
- query_params.pop("format")
- spm_filter = query_params.pop("spm") if query_params.get("spm") else None
- if not query_params and not spm_filter:
- display_message = _("Export all")
+ params = self._filter_params(query_params)
+
+ spm_filter = params.pop("spm", None)
+
+ if not params and not spm_filter:
+ display_message = gettext_noop("Export all")
elif spm_filter:
- display_message = _("Export only selected items")
+ display_message = gettext_noop("Export only selected items")
else:
query = ",".join(
- ["%s=%s" % (key, value) for key, value in query_params.items()]
+ ["%s=%s" % (key, value) for key, value in params.items()]
)
- display_message = _("Export filtered: %s") % query
+ display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
return display_message
+ def record_logs(self, ids, **kwargs):
+ resource_type = self.model._meta.verbose_name
+ create_or_update_operate_log(
+ self.ACTION, resource_type, force=True, **kwargs
+ )
+ detail = i18n_fmt(
+ gettext_noop('User %s view/export secret'), self.request.user
+ )
+ activities = [
+ ActivityLog(
+ resource_id=getattr(resource_id, 'pk', resource_id),
+ type=ActivityChoices.operate_log, detail=detail
+ )
+ for resource_id in ids
+ ]
+ ActivityLog.objects.bulk_create(activities)
+
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
with translation.override('en'):
resource_display = self.get_resource_display(request)
- resource_type = self.model._meta.verbose_name
- create_or_update_operate_log(
- self.ACTION, resource_type, force=True,
- resource_display=resource_display
- )
+ ids = [q.id for q in self.get_queryset()]
+ self.record_logs(ids, resource_display=resource_display)
return response
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
with translation.override('en'):
- resource_type = self.model._meta.verbose_name
- create_or_update_operate_log(
- self.ACTION, resource_type, force=True, resource=self.get_object()
- )
+ resource = self.get_object()
+ self.record_logs([resource.id], resource=resource)
return response
diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo
index 472b329b3..86fc7369d 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:6fa80b59b9b5f95a9cfcad8ec47eacd519bb962d139ab90463795a7b306a0a72
-size 137935
+oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
+size 138303
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index d4b4ecc20..772fb0b5c 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: 2023-03-14 17:34+0800\n"
+"POT-Creation-Date: 2023-04-07 13:57+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -70,6 +70,22 @@ msgstr "ローカル"
msgid "Collected"
msgstr ""
+#: accounts/const/account.py:24 ops/const.py:45
+msgid "Skip"
+msgstr "スキップ"
+
+#: accounts/const/account.py:25 audits/const.py:23 rbac/tree.py:229
+#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6
+msgid "Update"
+msgstr "更新"
+
+#: accounts/const/account.py:26
+#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53
+#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
+#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41
+msgid "Failed"
+msgstr "失敗しました"
+
#: accounts/const/automation.py:22 rbac/tree.py:50
msgid "Push account"
msgstr "アカウントプッシュ"
@@ -159,25 +175,26 @@ msgid "Only create"
msgstr "作成のみ"
#: accounts/models/account.py:47
-#: accounts/models/automations/gather_account.py:14
-#: accounts/serializers/account/account.py:95
+#: accounts/models/automations/gather_account.py:16
+#: accounts/serializers/account/account.py:173
+#: accounts/serializers/account/account.py:206
#: accounts/serializers/account/gathered_account.py:10
#: accounts/serializers/automations/change_secret.py:111
#: accounts/serializers/automations/change_secret.py:131
#: acls/models/base.py:100 acls/serializers/base.py:56
-#: assets/models/asset/common.py:92 assets/models/asset/common.py:280
+#: assets/models/asset/common.py:92 assets/models/asset/common.py:268
#: assets/models/cmd_filter.py:36 assets/serializers/domain.py:19
#: assets/serializers/label.py:27 audits/models.py:48
#: authentication/models/connection_token.py:33
-#: perms/models/asset_permission.py:64 perms/serializers/permission.py:35
+#: perms/models/asset_permission.py:64 perms/serializers/permission.py:34
#: terminal/backends/command/models.py:20 terminal/models/session/session.py:32
#: terminal/notifications.py:95 terminal/serializers/command.py:17
#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:212
msgid "Asset"
msgstr "資産"
-#: accounts/models/account.py:51 accounts/serializers/account/account.py:99
-#: authentication/serializers/connect_token_secret.py:50
+#: accounts/models/account.py:51 accounts/serializers/account/account.py:178
+#: authentication/serializers/connect_token_secret.py:48
msgid "Su from"
msgstr "から切り替え"
@@ -186,52 +203,58 @@ msgstr "から切り替え"
msgid "Version"
msgstr "バージョン"
-#: accounts/models/account.py:55 accounts/serializers/account/account.py:96
+#: accounts/models/account.py:55 accounts/serializers/account/account.py:174
#: users/models/user.py:768
msgid "Source"
msgstr "ソース"
-#: accounts/models/account.py:58
+#: accounts/models/account.py:56
+#, fuzzy
+#| msgid "Source"
+msgid "Source ID"
+msgstr "ソース"
+
+#: accounts/models/account.py:59
#: accounts/serializers/automations/change_secret.py:112
#: accounts/serializers/automations/change_secret.py:132
#: acls/models/base.py:102 acls/serializers/base.py:57
-#: assets/serializers/asset/common.py:131 assets/serializers/gateway.py:28
+#: assets/serializers/asset/common.py:107 assets/serializers/gateway.py:28
#: audits/models.py:49 ops/models/base.py:18
-#: perms/models/asset_permission.py:70 perms/serializers/permission.py:40
+#: perms/models/asset_permission.py:70 perms/serializers/permission.py:39
#: terminal/backends/command/models.py:21 terminal/models/session/session.py:34
#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85
msgid "Account"
msgstr "アカウント"
-#: accounts/models/account.py:64
+#: accounts/models/account.py:65
msgid "Can view asset account secret"
msgstr "資産アカウントの秘密を表示できます"
-#: accounts/models/account.py:65
+#: accounts/models/account.py:66
msgid "Can view asset history account"
msgstr "資産履歴アカウントを表示できます"
-#: accounts/models/account.py:66
+#: accounts/models/account.py:67
msgid "Can view asset history account secret"
msgstr "資産履歴アカウントパスワードを表示できます"
-#: accounts/models/account.py:67
+#: accounts/models/account.py:68
msgid "Can verify account"
msgstr "アカウントを確認できます"
-#: accounts/models/account.py:68
+#: accounts/models/account.py:69
msgid "Can push account"
msgstr "アカウントをプッシュできます"
-#: accounts/models/account.py:109
+#: accounts/models/account.py:110
msgid "Account template"
msgstr "アカウント テンプレート"
-#: accounts/models/account.py:114
+#: accounts/models/account.py:115
msgid "Can view asset account template secret"
msgstr "アセット アカウント テンプレートのパスワードを表示できます"
-#: accounts/models/account.py:115
+#: accounts/models/account.py:116
msgid "Can change asset account template secret"
msgstr "アセット アカウント テンプレートのパスワードを変更できます"
@@ -276,7 +299,7 @@ msgid "Trigger mode"
msgstr "トリガーモード"
#: accounts/models/automations/backup_account.py:97 audits/models.py:172
-#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:168
+#: terminal/models/session/sharing.py:111 xpack/plugins/cloud/models.py:168
msgid "Reason"
msgstr "理由"
@@ -328,16 +351,17 @@ msgid "Can add push account execution"
msgstr "プッシュ アカウントの作成の実行"
#: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36
-#: accounts/serializers/account/account.py:134
+#: accounts/serializers/account/account.py:383
#: accounts/serializers/account/base.py:16
#: accounts/serializers/automations/change_secret.py:46
-#: authentication/serializers/connect_token_secret.py:41
-#: authentication/serializers/connect_token_secret.py:51
+#: authentication/serializers/connect_token_secret.py:40
+#: authentication/serializers/connect_token_secret.py:49
msgid "Secret type"
msgstr "鍵の種類"
#: accounts/models/automations/change_secret.py:20
#: accounts/models/automations/change_secret.py:90 accounts/models/base.py:38
+#: accounts/serializers/account/base.py:19
#: authentication/models/temp_token.py:10
#: authentication/templates/authentication/_access_key_modal.html:31
#: settings/serializers/auth/radius.py:19
@@ -376,7 +400,8 @@ msgstr "開始日"
msgid "Date finished"
msgstr "終了日"
-#: accounts/models/automations/change_secret.py:94 assets/const/automation.py:8
+#: accounts/models/automations/change_secret.py:94
+#: accounts/serializers/account/account.py:208 assets/const/automation.py:8
#: common/const/choices.py:20
msgid "Error"
msgstr "間違い"
@@ -385,15 +410,15 @@ msgstr "間違い"
msgid "Change secret record"
msgstr "パスワード レコードの変更"
-#: accounts/models/automations/gather_account.py:12
+#: accounts/models/automations/gather_account.py:14
msgid "Present"
msgstr "存在する"
-#: accounts/models/automations/gather_account.py:13
+#: accounts/models/automations/gather_account.py:15
msgid "Date last login"
msgstr "最終ログイン日"
-#: accounts/models/automations/gather_account.py:15
+#: accounts/models/automations/gather_account.py:17
#: accounts/models/automations/push_account.py:15 accounts/models/base.py:34
#: acls/serializers/base.py:18 acls/serializers/base.py:49
#: assets/models/_user.py:23 audits/models.py:157 authentication/forms.py:25
@@ -406,15 +431,21 @@ msgstr "最終ログイン日"
msgid "Username"
msgstr "ユーザー名"
-#: accounts/models/automations/gather_account.py:16
+#: accounts/models/automations/gather_account.py:18
msgid "Address last login"
msgstr "最終ログインアドレス"
-#: accounts/models/automations/gather_account.py:23
+#: accounts/models/automations/gather_account.py:44
msgid "Gather account automation"
msgstr "自動収集アカウント"
-#: accounts/models/automations/gather_account.py:39
+#: accounts/models/automations/gather_account.py:56
+#, fuzzy
+#| msgid "Is service account"
+msgid "Is sync account"
+msgstr "サービスアカウントです"
+
+#: accounts/models/automations/gather_account.py:71
#: accounts/tasks/gather_accounts.py:29
msgid "Gather asset accounts"
msgstr "アカウントのコレクション"
@@ -426,7 +457,7 @@ msgstr "トリガー方式"
#: accounts/models/automations/push_account.py:16 acls/models/base.py:81
#: acls/serializers/base.py:81 acls/serializers/login_acl.py:25
#: assets/models/cmd_filter.py:81 audits/models.py:65 audits/serializers.py:82
-#: authentication/serializers/connect_token_secret.py:109
+#: authentication/serializers/connect_token_secret.py:107
#: authentication/templates/authentication/_access_key_modal.html:34
msgid "Action"
msgstr "アクション"
@@ -445,10 +476,10 @@ msgstr "アカウントの確認"
#: assets/models/asset/common.py:90 assets/models/asset/common.py:102
#: assets/models/cmd_filter.py:21 assets/models/domain.py:18
#: assets/models/group.py:20 assets/models/label.py:18
-#: assets/models/platform.py:21 assets/models/platform.py:76
-#: assets/serializers/asset/common.py:74 assets/serializers/asset/common.py:151
-#: assets/serializers/platform.py:133
-#: authentication/serializers/connect_token_secret.py:103 ops/mixin.py:21
+#: assets/models/platform.py:13 assets/models/platform.py:65
+#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93
+#: assets/serializers/platform.py:184
+#: authentication/serializers/connect_token_secret.py:101 ops/mixin.py:21
#: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57
#: ops/models/job.py:91 ops/models/playbook.py:23 ops/serializers/job.py:19
#: orgs/models.py:69 perms/models/asset_permission.py:56 rbac/models/role.py:29
@@ -456,7 +487,7 @@ msgstr "アカウントの確認"
#: terminal/models/applet/applet.py:27 terminal/models/component/endpoint.py:12
#: terminal/models/component/endpoint.py:90
#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15
-#: terminal/models/component/terminal.py:79 users/forms/profile.py:33
+#: terminal/models/component/terminal.py:85 users/forms/profile.py:33
#: users/models/group.py:13 users/models/user.py:717
#: xpack/plugins/cloud/models.py:28
msgid "Name"
@@ -469,7 +500,7 @@ msgstr "特権アカウント"
#: accounts/models/base.py:40 assets/models/asset/common.py:109
#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39
#: assets/models/label.py:22
-#: authentication/serializers/connect_token_secret.py:107
+#: authentication/serializers/connect_token_secret.py:105
#: terminal/models/applet/applet.py:32 users/serializers/user.py:162
msgid "Is active"
msgstr "アクティブです。"
@@ -515,38 +546,38 @@ msgstr ""
"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人"
"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください"
-#: accounts/serializers/account/account.py:65
-#: assets/serializers/asset/common.py:72 settings/serializers/auth/sms.py:75
+#: accounts/serializers/account/account.py:26
+#: settings/serializers/auth/sms.py:75
msgid "Template"
msgstr "テンプレート"
-#: accounts/serializers/account/account.py:68
-#: assets/serializers/asset/common.py:69
+#: accounts/serializers/account/account.py:29
msgid "Push now"
msgstr "今すぐプッシュ"
-#: accounts/serializers/account/account.py:70
-#: accounts/serializers/account/base.py:64
-msgid "Has secret"
-msgstr "エスクローされたパスワード"
+#: accounts/serializers/account/account.py:33
+#, fuzzy
+#| msgid "Runas policy"
+msgid "Exist policy"
+msgstr "ユーザー ポリシー"
-#: accounts/serializers/account/account.py:75 applications/models.py:11
-#: assets/models/label.py:21 assets/models/platform.py:77
-#: assets/serializers/asset/common.py:127 assets/serializers/cagegory.py:8
-#: assets/serializers/platform.py:94 assets/serializers/platform.py:134
+#: accounts/serializers/account/account.py:153 applications/models.py:11
+#: assets/models/label.py:21 assets/models/platform.py:66
+#: assets/serializers/asset/common.py:103 assets/serializers/cagegory.py:8
+#: assets/serializers/platform.py:111 assets/serializers/platform.py:185
#: perms/serializers/user_permission.py:26 settings/models.py:35
#: tickets/models/ticket/apply_application.py:13
msgid "Category"
msgstr "カテゴリ"
-#: accounts/serializers/account/account.py:76
+#: accounts/serializers/account/account.py:154
#: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24
#: acls/serializers/command_acl.py:18 applications/models.py:14
#: assets/models/_user.py:50 assets/models/automations/base.py:20
-#: assets/models/cmd_filter.py:74 assets/models/platform.py:78
-#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93
-#: audits/serializers.py:48
-#: authentication/serializers/connect_token_secret.py:116 ops/models/job.py:102
+#: assets/models/cmd_filter.py:74 assets/models/platform.py:67
+#: assets/serializers/asset/common.py:104 assets/serializers/platform.py:95
+#: assets/serializers/platform.py:110 audits/serializers.py:48
+#: authentication/serializers/connect_token_secret.py:114 ops/models/job.py:102
#: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:31
#: terminal/models/component/storage.py:57
#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29
@@ -558,10 +589,79 @@ msgstr "カテゴリ"
msgid "Type"
msgstr "タイプ"
-#: accounts/serializers/account/account.py:91
+#: accounts/serializers/account/account.py:169
msgid "Asset not found"
msgstr "資産が存在しません"
+#: accounts/serializers/account/account.py:175
+#: accounts/serializers/account/base.py:64
+msgid "Has secret"
+msgstr "エスクローされたパスワード"
+
+#: accounts/serializers/account/account.py:207 ops/models/celery.py:60
+#: tickets/models/comment.py:13 tickets/models/ticket/general.py:45
+#: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14
+#: tickets/serializers/ticket/ticket.py:21
+msgid "State"
+msgstr "状態"
+
+#: accounts/serializers/account/account.py:209
+#, fuzzy
+#| msgid "Change by"
+msgid "Changed"
+msgstr "による変更"
+
+#: accounts/serializers/account/account.py:213
+#: accounts/serializers/automations/base.py:22
+#: assets/models/automations/base.py:19
+#: assets/serializers/automations/base.py:20 ops/models/base.py:17
+#: ops/models/job.py:104 ops/serializers/job.py:20
+#: terminal/templates/terminal/_msg_command_execute_alert.html:16
+msgid "Assets"
+msgstr "資産"
+
+#: accounts/serializers/account/account.py:284
+#, fuzzy
+#| msgid "Name already exists"
+msgid "Account already exists"
+msgstr "名前は既に存在します。"
+
+#: accounts/serializers/account/account.py:320
+#, fuzzy, python-format
+#| msgid "Current user not support mfa type: {}"
+msgid "Asset does not support this secret type: %s"
+msgstr "現在のユーザーはmfaタイプをサポートしていません: {}"
+
+#: accounts/serializers/account/account.py:351
+#, fuzzy
+#| msgid "Account name"
+msgid "Account has exist"
+msgstr "アカウント名"
+
+#: accounts/serializers/account/account.py:393 acls/models/base.py:98
+#: acls/models/login_acl.py:13 acls/serializers/base.py:55
+#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:24
+#: assets/models/label.py:16 audits/models.py:44 audits/models.py:63
+#: audits/models.py:141 authentication/models/connection_token.py:29
+#: authentication/models/sso_token.py:16
+#: notifications/models/notification.py:12
+#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58
+#: perms/serializers/permission.py:30 rbac/builtin.py:122
+#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19
+#: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32
+#: terminal/notifications.py:96 terminal/notifications.py:144
+#: terminal/serializers/command.py:16 tickets/models/comment.py:21
+#: users/const.py:14 users/models/user.py:911 users/models/user.py:942
+#: users/serializers/group.py:18
+msgid "User"
+msgstr "ユーザー"
+
+#: accounts/serializers/account/account.py:394
+#: authentication/templates/authentication/_access_key_modal.html:33
+#: terminal/notifications.py:98 terminal/notifications.py:146
+msgid "Date"
+msgstr "日付"
+
#: accounts/serializers/account/backup.py:31
#: accounts/serializers/automations/base.py:36
#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:103
@@ -583,27 +683,15 @@ msgstr "現在、メール送信のみがサポートされています"
msgid "Asset type"
msgstr "資産タイプ"
-#: accounts/serializers/account/base.py:19
-msgid "Secret/Password"
-msgstr "キー/パスワード"
-
#: accounts/serializers/account/base.py:24
msgid "Key password"
msgstr "キーパスワード"
-#: accounts/serializers/account/base.py:81
-#: assets/serializers/asset/common.py:301
+#: accounts/serializers/account/base.py:80
+#: assets/serializers/asset/common.py:298
msgid "Spec info"
msgstr "特別情報"
-#: accounts/serializers/automations/base.py:22
-#: assets/models/automations/base.py:19
-#: assets/serializers/automations/base.py:20 ops/models/base.py:17
-#: ops/models/job.py:104 ops/serializers/job.py:20
-#: terminal/templates/terminal/_msg_command_execute_alert.html:16
-msgid "Assets"
-msgstr "資産"
-
#: accounts/serializers/automations/base.py:23
#: assets/models/asset/common.py:108 assets/models/automations/base.py:18
#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21
@@ -641,17 +729,11 @@ msgstr "自動タスク実行履歴"
#: accounts/serializers/automations/change_secret.py:154 audits/const.py:52
#: audits/models.py:54 audits/signal_handlers/activity_log.py:33
#: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:39
-#: terminal/const.py:59 terminal/models/session/sharing.py:103
+#: terminal/const.py:59 terminal/models/session/sharing.py:107
#: tickets/views/approve.py:114
msgid "Success"
msgstr "成功"
-#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53
-#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
-#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41
-msgid "Failed"
-msgstr "失敗しました"
-
#: accounts/tasks/automation.py:24
msgid "Account execute automation"
msgstr "アカウント実行の自動化"
@@ -721,7 +803,7 @@ msgstr "1-100、低い値は最初に一致します"
#: acls/models/base.py:82 acls/serializers/base.py:75
#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86
-#: authentication/serializers/connect_token_secret.py:82
+#: authentication/serializers/connect_token_secret.py:79
msgid "Reviewers"
msgstr "レビュー担当者"
@@ -733,24 +815,6 @@ msgstr "レビュー担当者"
msgid "Active"
msgstr "アクティブ"
-#: acls/models/base.py:98 acls/models/login_acl.py:13
-#: acls/serializers/base.py:55 acls/serializers/login_acl.py:21
-#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:44
-#: audits/models.py:63 audits/models.py:141
-#: authentication/models/connection_token.py:29
-#: authentication/models/sso_token.py:16
-#: notifications/models/notification.py:12
-#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58
-#: perms/serializers/permission.py:31 rbac/builtin.py:122
-#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19
-#: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32
-#: terminal/notifications.py:96 terminal/notifications.py:144
-#: terminal/serializers/command.py:16 tickets/models/comment.py:21
-#: users/const.py:14 users/models/user.py:911 users/models/user.py:942
-#: users/serializers/group.py:18
-msgid "User"
-msgstr "ユーザー"
-
#: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
#: ops/serializers/job.py:65 terminal/const.py:67
#: terminal/models/session/session.py:43 terminal/serializers/command.py:18
@@ -778,7 +842,7 @@ msgstr "家を無視する"
#: acls/models/command_acl.py:33 acls/models/command_acl.py:96
#: acls/serializers/command_acl.py:28
-#: authentication/serializers/connect_token_secret.py:79
+#: authentication/serializers/connect_token_secret.py:76
msgid "Command group"
msgstr "コマンドグループ"
@@ -883,7 +947,7 @@ msgstr ""
#: authentication/templates/authentication/_msg_oauth_bind.html:12
#: authentication/templates/authentication/_msg_rest_password_success.html:8
#: authentication/templates/authentication/_msg_rest_public_key_success.html:8
-#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61
+#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:62
msgid "IP"
msgstr "IP"
@@ -908,7 +972,7 @@ msgstr "アプリケーション"
msgid "Can match application"
msgstr "アプリケーションを一致させることができます"
-#: assets/api/asset/asset.py:143
+#: assets/api/asset/asset.py:147
msgid "Cannot create asset directly, you should create a host or other"
msgstr ""
"資産を直接作成することはできません。ホストまたはその他を作成する必要がありま"
@@ -984,31 +1048,37 @@ msgstr "テストゲートウェイ"
msgid "Gather facts"
msgstr "資産情報の収集"
-#: assets/const/category.py:11 assets/models/asset/host.py:8
+#: assets/const/category.py:10 assets/models/asset/host.py:8
#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67
#: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17
#: xpack/plugins/cloud/serializers/account_attrs.py:72
msgid "Host"
msgstr "ホスト"
-#: assets/const/category.py:12 assets/models/asset/device.py:8
+#: assets/const/category.py:11 assets/models/asset/device.py:8
msgid "Device"
msgstr "インターネット機器"
-#: assets/const/category.py:13 assets/models/asset/database.py:9
-#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:115
+#: assets/const/category.py:12 assets/models/asset/database.py:9
+#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:91
msgid "Database"
msgstr "データベース"
-#: assets/const/category.py:14
+#: assets/const/category.py:13
msgid "Cloud service"
msgstr "クラウド サービス"
-#: assets/const/category.py:15 assets/models/asset/web.py:16 audits/const.py:33
+#: assets/const/category.py:14 assets/models/asset/web.py:16 audits/const.py:33
#: terminal/models/applet/applet.py:25
msgid "Web"
msgstr "Web"
+#: assets/const/category.py:15
+#, fuzzy
+#| msgid "Custom user"
+msgid "Custom type"
+msgstr "カスタムユーザー"
+
#: assets/const/cloud.py:7
msgid "Public cloud"
msgstr "パブリック クラウド"
@@ -1042,7 +1112,7 @@ msgstr "ファイアウォール"
msgid "Other"
msgstr "その他"
-#: assets/const/types.py:200
+#: assets/const/types.py:214
msgid "All types"
msgstr "いろんなタイプ"
@@ -1060,7 +1130,7 @@ msgid "Basic"
msgstr "基本"
#: assets/const/web.py:61 assets/models/asset/web.py:13
-#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:40
+#: assets/serializers/asset/common.py:99 assets/serializers/platform.py:40
msgid "Script"
msgstr "脚本"
@@ -1080,7 +1150,7 @@ msgstr "SSHパブリックキー"
#: assets/models/cmd_filter.py:88 assets/models/group.py:23
#: common/db/models.py:37 ops/models/adhoc.py:27 ops/models/job.py:110
#: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38
-#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:158
+#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:160
#: terminal/models/applet/host.py:111 terminal/models/component/endpoint.py:24
#: terminal/models/component/endpoint.py:100
#: terminal/models/session/session.py:47 tickets/models/comment.py:32
@@ -1129,7 +1199,7 @@ msgid "Username same with user"
msgstr "ユーザーと同じユーザー名"
#: assets/models/_user.py:52 authentication/models/connection_token.py:38
-#: authentication/serializers/connect_token_secret.py:104
+#: authentication/serializers/connect_token_secret.py:102
#: terminal/models/applet/applet.py:34 terminal/serializers/session.py:20
#: terminal/serializers/session.py:41 terminal/serializers/storage.py:68
msgid "Protocol"
@@ -1183,25 +1253,25 @@ msgstr "システムユーザーに一致できます"
msgid "Cloud"
msgstr "クラウド サービス"
-#: assets/models/asset/common.py:91 assets/models/platform.py:22
+#: assets/models/asset/common.py:91 assets/models/platform.py:14
#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68
#: xpack/plugins/cloud/serializers/account_attrs.py:73
msgid "Port"
msgstr "ポート"
-#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:152
+#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:129
msgid "Address"
msgstr "アドレス"
-#: assets/models/asset/common.py:104 assets/models/platform.py:112
-#: authentication/serializers/connect_token_secret.py:108
+#: assets/models/asset/common.py:104 assets/models/platform.py:100
+#: authentication/serializers/connect_token_secret.py:106
#: perms/serializers/user_permission.py:24
#: xpack/plugins/cloud/serializers/account_attrs.py:187
msgid "Platform"
msgstr "プラットフォーム"
#: assets/models/asset/common.py:106 assets/models/domain.py:21
-#: authentication/serializers/connect_token_secret.py:126
+#: authentication/serializers/connect_token_secret.py:124
#: perms/serializers/user_permission.py:28
msgid "Domain"
msgstr "ドメイン"
@@ -1210,23 +1280,42 @@ msgstr "ドメイン"
msgid "Labels"
msgstr "ラベル"
-#: assets/models/asset/common.py:283
+#: assets/models/asset/common.py:111
+#, fuzzy
+#| msgid "Gather asset hardware info"
+msgid "Gathered info"
+msgstr "資産ハードウェア情報の収集"
+
+#: assets/models/asset/common.py:112 assets/serializers/asset/common.py:109
+#: assets/serializers/asset/common.py:179
+#, fuzzy
+#| msgid "Auto info"
+msgid "Custom info"
+msgstr "自動情報"
+
+#: assets/models/asset/common.py:271
msgid "Can refresh asset hardware info"
msgstr "資産ハードウェア情報を更新できます"
-#: assets/models/asset/common.py:284
+#: assets/models/asset/common.py:272
msgid "Can test asset connectivity"
msgstr "資産接続をテストできます"
-#: assets/models/asset/common.py:285
+#: assets/models/asset/common.py:273
msgid "Can match asset"
msgstr "アセットを一致させることができます"
-#: assets/models/asset/common.py:286
+#: assets/models/asset/common.py:274
msgid "Can change asset nodes"
msgstr "資産ノードを変更できます"
-#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:116
+#: assets/models/asset/custom.py:8
+#, fuzzy
+#| msgid "Custom user"
+msgid "Custom asset"
+msgstr "カスタムユーザー"
+
+#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:92
#: settings/serializers/email.py:37
msgid "Use SSL"
msgstr "SSLの使用"
@@ -1243,7 +1332,7 @@ msgstr "クライアント証明書"
msgid "Client key"
msgstr "クライアントキー"
-#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:117
+#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:93
msgid "Allow invalid cert"
msgstr "証明書チェックを無視"
@@ -1251,23 +1340,23 @@ msgstr "証明書チェックを無視"
msgid "Autofill"
msgstr "自動充填"
-#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:120
+#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:96
#: assets/serializers/platform.py:32
msgid "Username selector"
msgstr "ユーザー名ピッカー"
-#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:121
+#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:97
#: assets/serializers/platform.py:35
msgid "Password selector"
msgstr "パスワードセレクター"
-#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:122
+#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:98
#: assets/serializers/platform.py:38
msgid "Submit selector"
msgstr "ボタンセレクターを確認する"
#: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38
-#: assets/serializers/asset/common.py:300 rbac/tree.py:35
+#: assets/serializers/asset/common.py:297 rbac/tree.py:35
msgid "Accounts"
msgstr "アカウント"
@@ -1281,8 +1370,8 @@ msgstr "アセットの自動化タスク"
#: assets/models/automations/base.py:112 audits/models.py:177
#: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:183
-#: terminal/models/applet/applet.py:157 terminal/models/applet/host.py:108
-#: terminal/models/component/status.py:27 terminal/serializers/applet.py:18
+#: terminal/models/applet/applet.py:159 terminal/models/applet/host.py:108
+#: terminal/models/component/status.py:30 terminal/serializers/applet.py:18
#: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283
#: tickets/serializers/super_ticket.py:13
#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:164
@@ -1307,7 +1396,7 @@ msgid "Date verified"
msgstr "確認済みの日付"
#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61
-#: perms/serializers/permission.py:33 users/models/group.py:25
+#: perms/serializers/permission.py:32 users/models/group.py:25
#: users/models/user.py:723
msgid "User group"
msgstr "ユーザーグループ"
@@ -1340,7 +1429,7 @@ msgstr "コマンドフィルタルール"
msgid "Favorite asset"
msgstr "お気に入りのアセット"
-#: assets/models/gateway.py:35 assets/serializers/domain.py:16
+#: assets/models/gateway.py:34 assets/serializers/domain.py:16
msgid "Gateway"
msgstr "ゲートウェイ"
@@ -1348,7 +1437,8 @@ msgstr "ゲートウェイ"
msgid "Asset group"
msgstr "資産グループ"
-#: assets/models/group.py:34 assets/models/platform.py:19
+#: assets/models/group.py:34 assets/models/platform.py:17
+#: assets/serializers/platform.py:97
#: xpack/plugins/cloud/providers/nutanix.py:30
msgid "Default"
msgstr "デフォルト"
@@ -1364,14 +1454,15 @@ msgstr "システム"
#: assets/models/label.py:19 assets/models/node.py:544
#: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14
#: authentication/models/connection_token.py:26
-#: authentication/serializers/connect_token_secret.py:115
+#: authentication/serializers/connect_token_secret.py:113
#: common/serializers/common.py:80 settings/models.py:34
msgid "Value"
msgstr "値"
-#: assets/models/label.py:40 assets/serializers/asset/common.py:129
+#: assets/models/label.py:40 assets/serializers/asset/common.py:105
#: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13
-#: authentication/serializers/connect_token_secret.py:114
+#: assets/serializers/platform.py:94
+#: authentication/serializers/connect_token_secret.py:112
#: common/serializers/common.py:79 settings/serializers/sms.py:7
msgid "Label"
msgstr "ラベル"
@@ -1396,7 +1487,7 @@ msgstr "フルバリュー"
msgid "Parent key"
msgstr "親キー"
-#: assets/models/node.py:558 perms/serializers/permission.py:36
+#: assets/models/node.py:558 perms/serializers/permission.py:35
#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96
msgid "Node"
msgstr "ノード"
@@ -1405,129 +1496,153 @@ msgstr "ノード"
msgid "Can match node"
msgstr "ノードを一致させることができます"
-#: assets/models/platform.py:20
+#: assets/models/platform.py:15
+msgid "Primary"
+msgstr "主要"
+
+#: assets/models/platform.py:16
msgid "Required"
msgstr "必要"
-#: assets/models/platform.py:23 settings/serializers/settings.py:65
+#: assets/models/platform.py:18
+#, fuzzy
+#| msgid "Public IP"
+msgid "Public"
+msgstr "パブリックIP"
+
+#: assets/models/platform.py:19 settings/serializers/settings.py:65
#: users/templates/users/reset_password.html:29
msgid "Setting"
msgstr "設定"
-#: assets/models/platform.py:42 audits/const.py:47 settings/models.py:37
+#: assets/models/platform.py:31 audits/const.py:47 settings/models.py:37
#: terminal/serializers/applet_host.py:29
msgid "Enabled"
msgstr "有効化"
-#: assets/models/platform.py:43
+#: assets/models/platform.py:32
msgid "Ansible config"
msgstr "Ansible 構成"
-#: assets/models/platform.py:44 assets/serializers/platform.py:61
+#: assets/models/platform.py:33 assets/serializers/platform.py:60
msgid "Ping enabled"
msgstr "アセット ディスカバリを有効にする"
-#: assets/models/platform.py:45 assets/serializers/platform.py:62
+#: assets/models/platform.py:34 assets/serializers/platform.py:61
msgid "Ping method"
msgstr "資産検出方法"
-#: assets/models/platform.py:46 assets/models/platform.py:59
-#: assets/serializers/platform.py:63
+#: assets/models/platform.py:35 assets/models/platform.py:48
+#: assets/serializers/platform.py:62
msgid "Gather facts enabled"
msgstr "資産情報の収集を有効にする"
-#: assets/models/platform.py:47 assets/models/platform.py:61
-#: assets/serializers/platform.py:64
+#: assets/models/platform.py:36 assets/models/platform.py:50
+#: assets/serializers/platform.py:63
msgid "Gather facts method"
msgstr "情報収集の方法"
-#: assets/models/platform.py:48 assets/serializers/platform.py:67
+#: assets/models/platform.py:37 assets/serializers/platform.py:66
msgid "Change secret enabled"
msgstr "パスワードの変更が有効"
-#: assets/models/platform.py:50 assets/serializers/platform.py:68
+#: assets/models/platform.py:39 assets/serializers/platform.py:67
msgid "Change secret method"
msgstr "パスワード変更モード"
-#: assets/models/platform.py:52 assets/serializers/platform.py:69
+#: assets/models/platform.py:41 assets/serializers/platform.py:68
msgid "Push account enabled"
msgstr "アカウントのプッシュを有効にする"
-#: assets/models/platform.py:54 assets/serializers/platform.py:70
+#: assets/models/platform.py:43 assets/serializers/platform.py:69
msgid "Push account method"
msgstr "アカウントプッシュ方式"
-#: assets/models/platform.py:56 assets/serializers/platform.py:65
+#: assets/models/platform.py:45 assets/serializers/platform.py:64
msgid "Verify account enabled"
msgstr "アカウントの確認をオンにする"
-#: assets/models/platform.py:58 assets/serializers/platform.py:66
+#: assets/models/platform.py:47 assets/serializers/platform.py:65
msgid "Verify account method"
msgstr "アカウント認証方法"
-#: assets/models/platform.py:79 tickets/models/ticket/general.py:300
+#: assets/models/platform.py:68 tickets/models/ticket/general.py:300
msgid "Meta"
msgstr "メタ"
-#: assets/models/platform.py:80
+#: assets/models/platform.py:69
msgid "Internal"
msgstr "ビルトイン"
-#: assets/models/platform.py:83 assets/serializers/platform.py:91
+#: assets/models/platform.py:73 assets/serializers/platform.py:109
msgid "Charset"
msgstr "シャーセット"
-#: assets/models/platform.py:85 assets/serializers/platform.py:119
+#: assets/models/platform.py:75 assets/serializers/platform.py:133
msgid "Domain enabled"
msgstr "ドメインを有効にする"
-#: assets/models/platform.py:87 assets/serializers/platform.py:118
+#: assets/models/platform.py:77 assets/serializers/platform.py:132
msgid "Su enabled"
msgstr "アカウントの切り替えを有効にする"
-#: assets/models/platform.py:88 assets/serializers/platform.py:101
+#: assets/models/platform.py:78 assets/serializers/platform.py:115
msgid "Su method"
msgstr "アカウントの切り替え方法"
-#: assets/models/platform.py:90 assets/serializers/platform.py:98
+#: assets/models/platform.py:81 assets/serializers/platform.py:113
msgid "Automation"
msgstr "オートメーション"
+#: assets/models/platform.py:83 assets/serializers/platform.py:118
+#, fuzzy
+#| msgid "Custom user"
+msgid "Custom fields"
+msgstr "カスタムユーザー"
+
#: assets/models/utils.py:18
#, python-format
msgid "%(value)s is not an even number"
msgstr "%(value)s は偶数ではありません"
-#: assets/serializers/asset/common.py:119
+#: assets/notifications.py:12
+msgid ""
+"Batch update platform in assets, skipping assets that do not meet platform "
+"type"
+msgstr ""
+"プラットフォームタイプがスキップされた資産に合致しない、資産内の一括更新プ"
+"ラットフォーム"
+
+#: assets/serializers/asset/common.py:95
msgid "Auto fill"
msgstr "自動充填"
-#: assets/serializers/asset/common.py:130 assets/serializers/platform.py:96
-#: authentication/serializers/connect_token_secret.py:28
-#: authentication/serializers/connect_token_secret.py:66
+#: assets/serializers/asset/common.py:106 assets/serializers/platform.py:112
+#: authentication/serializers/connect_token_secret.py:29
+#: authentication/serializers/connect_token_secret.py:63
#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99
msgid "Protocols"
msgstr "プロトコル"
-#: assets/serializers/asset/common.py:132
-#: assets/serializers/asset/common.py:153
+#: assets/serializers/asset/common.py:108
+#: assets/serializers/asset/common.py:130
msgid "Node path"
msgstr "ノードパスです"
-#: assets/serializers/asset/common.py:150
-#: assets/serializers/asset/common.py:302
+#: assets/serializers/asset/common.py:127
+#: assets/serializers/asset/common.py:299
msgid "Auto info"
msgstr "自動情報"
-#: assets/serializers/asset/common.py:226
+#: assets/serializers/asset/common.py:220
msgid "Platform not exist"
msgstr "プラットフォームが存在しません"
-#: assets/serializers/asset/common.py:261
+#: assets/serializers/asset/common.py:255
msgid "port out of range (1-65535)"
msgstr "ポート番号が範囲外です (1-65535)"
-#: assets/serializers/asset/common.py:268
+#: assets/serializers/asset/common.py:262
msgid "Protocol is required: {}"
msgstr "プロトコルが必要です: {}"
@@ -1576,7 +1691,7 @@ msgid "Disk total"
msgstr "ディスクの合計"
#: assets/serializers/asset/host.py:21
-#: authentication/serializers/connect_token_secret.py:105
+#: authentication/serializers/connect_token_secret.py:103
msgid "OS"
msgstr "OS"
@@ -1628,26 +1743,34 @@ msgstr "SFTP が有効"
msgid "SFTP home"
msgstr "SFTP ルート パス"
-#: assets/serializers/platform.py:43
+#: assets/serializers/platform.py:42
msgid "Auth with username"
msgstr "ユーザー名で認証する"
-#: assets/serializers/platform.py:71
+#: assets/serializers/platform.py:70
msgid "Gather accounts enabled"
msgstr "アカウント収集を有効にする"
-#: assets/serializers/platform.py:72
+#: assets/serializers/platform.py:71
msgid "Gather accounts method"
msgstr "アカウントの収集方法"
-#: assets/serializers/platform.py:78
-msgid "Primary"
-msgstr "主要"
-
-#: assets/serializers/platform.py:120
+#: assets/serializers/platform.py:134
msgid "Default Domain"
msgstr "デフォルト ドメイン"
+#: assets/serializers/platform.py:143
+#, fuzzy
+#| msgid "test_phone is required"
+msgid "type is required"
+msgstr "携帯番号をテストこのフィールドは必須です"
+
+#: assets/serializers/platform.py:173
+#, fuzzy
+#| msgid "Protocol is required: {}"
+msgid "Protocols is required"
+msgstr "プロトコルが必要です: {}"
+
#: assets/signal_handlers/asset.py:26 assets/tasks/ping.py:35
msgid "Test assets connectivity "
msgstr "アセット接続のテスト。"
@@ -1762,11 +1885,6 @@ msgstr "ダウンロード"
msgid "View"
msgstr "表示"
-#: audits/const.py:23 rbac/tree.py:229 templates/_csv_import_export.html:18
-#: templates/_csv_update_modal.html:6
-msgid "Update"
-msgstr "更新"
-
#: audits/const.py:25
#: authentication/templates/authentication/_access_key_modal.html:22
#: rbac/tree.py:227
@@ -1788,7 +1906,7 @@ msgid "Change password"
msgstr "パスワードを変更する"
#: audits/const.py:34 settings/serializers/terminal.py:6
-#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:156
+#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:163
#: terminal/serializers/session.py:48
msgid "Terminal"
msgstr "ターミナル"
@@ -1827,7 +1945,7 @@ msgid "Job audit log"
msgstr "ジョブ監査ログ"
#: audits/models.py:46 audits/models.py:73 audits/models.py:144
-#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:95
+#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:99
msgid "Remote addr"
msgstr "リモートaddr"
@@ -2463,23 +2581,23 @@ msgstr "異なる都市ログインのリマインダー"
msgid "binding reminder"
msgstr "バインディングリマインダー"
-#: authentication/serializers/connect_token_secret.py:106
+#: authentication/serializers/connect_token_secret.py:104
msgid "Is builtin"
msgstr "ビルトイン"
-#: authentication/serializers/connect_token_secret.py:110
+#: authentication/serializers/connect_token_secret.py:108
msgid "Options"
msgstr "オプション"
-#: authentication/serializers/connect_token_secret.py:117
+#: authentication/serializers/connect_token_secret.py:115
msgid "Component"
msgstr "コンポーネント"
-#: authentication/serializers/connect_token_secret.py:128
+#: authentication/serializers/connect_token_secret.py:126
msgid "Expired now"
msgstr "すぐに期限切れ"
-#: authentication/serializers/connect_token_secret.py:148
+#: authentication/serializers/connect_token_secret.py:145
#: authentication/templates/authentication/_access_key_modal.html:30
#: perms/models/perm_node.py:21 users/serializers/group.py:33
msgid "ID"
@@ -2494,15 +2612,15 @@ msgid "Ticket info"
msgstr "作業指示情報"
#: authentication/serializers/connection_token.py:20
-#: perms/models/asset_permission.py:71 perms/serializers/permission.py:37
-#: perms/serializers/permission.py:70
+#: perms/models/asset_permission.py:71 perms/serializers/permission.py:36
+#: perms/serializers/permission.py:69
#: tickets/models/ticket/apply_application.py:28
#: tickets/models/ticket/apply_asset.py:18
msgid "Actions"
msgstr "アクション"
#: authentication/serializers/connection_token.py:41
-#: perms/serializers/permission.py:39 perms/serializers/permission.py:71
+#: perms/serializers/permission.py:38 perms/serializers/permission.py:70
#: users/serializers/user.py:93 users/serializers/user.py:165
msgid "Is expired"
msgstr "期限切れです"
@@ -2522,8 +2640,8 @@ msgstr "メール"
msgid "The {} cannot be empty"
msgstr "{} 空にしてはならない"
-#: authentication/serializers/token.py:79 perms/serializers/permission.py:38
-#: perms/serializers/permission.py:72 users/serializers/user.py:94
+#: authentication/serializers/token.py:79 perms/serializers/permission.py:37
+#: perms/serializers/permission.py:71 users/serializers/user.py:94
#: users/serializers/user.py:163
msgid "Is valid"
msgstr "有効です"
@@ -2544,11 +2662,6 @@ msgstr "APIキー記号APIヘッダーを使用すると、すべてのリクエ
msgid "docs"
msgstr "ドキュメント"
-#: authentication/templates/authentication/_access_key_modal.html:33
-#: terminal/notifications.py:98 terminal/notifications.py:146
-msgid "Date"
-msgstr "日付"
-
#: authentication/templates/authentication/_access_key_modal.html:48
msgid "Show"
msgstr "表示"
@@ -2664,7 +2777,7 @@ msgid "request new one"
msgstr "新しいものを要求する"
#: authentication/templates/authentication/_msg_reset_password_code.html:12
-#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:79
+#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:83
#: users/forms/profile.py:104 users/templates/users/forgot_password.html:65
msgid "Verify code"
msgstr "コードの確認"
@@ -2991,7 +3104,7 @@ msgstr "組織 ID"
msgid "The file content overflowed (The maximum length `{}` bytes)"
msgstr "ファイルの内容がオーバーフローしました (最大長 '{}' バイト)"
-#: common/drf/parsers/base.py:189
+#: common/drf/parsers/base.py:193
msgid "Parse file error: {}"
msgstr "解析ファイルエラー: {}"
@@ -3153,19 +3266,24 @@ msgstr "特殊文字を含むべきではない"
msgid "The mobile phone number format is incorrect"
msgstr "携帯電話番号の形式が正しくありません"
-#: common/views/mixins.py:58
+#: common/views/mixins.py:73
msgid "Export all"
msgstr "すべてエクスポート"
-#: common/views/mixins.py:60
+#: common/views/mixins.py:75
msgid "Export only selected items"
msgstr "選択項目のみエクスポート"
-#: common/views/mixins.py:65
+#: common/views/mixins.py:80
#, python-format
msgid "Export filtered: %s"
msgstr "検索のエクスポート: %s"
+#: common/views/mixins.py:89
+#, python-format
+msgid "User %s view/export secret"
+msgstr "ユーザー %s がパスワードを閲覧/導き出しました"
+
#: jumpserver/conf.py:416
msgid "Create account successfully"
msgstr "アカウントを正常に作成"
@@ -3242,11 +3360,11 @@ msgstr "投稿サイトニュース"
msgid "No account available"
msgstr "利用可能なアカウントがありません"
-#: ops/ansible/inventory.py:189
+#: ops/ansible/inventory.py:209
msgid "Ansible disabled"
msgstr "Ansible 無効"
-#: ops/ansible/inventory.py:205
+#: ops/ansible/inventory.py:225
msgid "Skip hosts below:"
msgstr "次のホストをスキップします: "
@@ -3322,10 +3440,6 @@ msgstr "特権アカウントのみ"
msgid "Privileged First"
msgstr "特権アカウント優先"
-#: ops/const.py:45
-msgid "Skip"
-msgstr "スキップ"
-
#: ops/const.py:50
msgid "Powershell"
msgstr "PowerShell"
@@ -3423,14 +3537,7 @@ msgstr "タスクモニターを表示できます"
msgid "Kwargs"
msgstr "クワーグ"
-#: ops/models/celery.py:60 tickets/models/comment.py:13
-#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279
-#: tickets/serializers/super_ticket.py:14
-#: tickets/serializers/ticket/ticket.py:21
-msgid "State"
-msgstr "状態"
-
-#: ops/models/celery.py:61 terminal/models/session/sharing.py:110
+#: ops/models/celery.py:61 terminal/models/session/sharing.py:114
#: tickets/const.py:25
msgid "Finished"
msgstr "終了"
@@ -3943,7 +4050,7 @@ msgid "My assets"
msgstr "私の資産"
#: rbac/tree.py:56 terminal/models/applet/applet.py:43
-#: terminal/models/applet/applet.py:154 terminal/models/applet/host.py:28
+#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:28
#: terminal/serializers/applet.py:15
msgid "Applet"
msgstr "リモートアプリケーション"
@@ -4919,43 +5026,39 @@ msgid "Remember manual auth"
msgstr "手動入力パスワードの保存"
#: settings/serializers/security.py:172
-msgid "Enable change auth secure mode"
-msgstr "安全モードの変更を有効にする"
-
-#: settings/serializers/security.py:175
msgid "Insecure command alert"
msgstr "安全でないコマンドアラート"
-#: settings/serializers/security.py:178
+#: settings/serializers/security.py:175
msgid "Email recipient"
msgstr "メール受信者"
-#: settings/serializers/security.py:179
+#: settings/serializers/security.py:176
msgid "Multiple user using , split"
msgstr "複数のユーザーを使用して、分割"
-#: settings/serializers/security.py:182
+#: settings/serializers/security.py:179
msgid "Operation center"
msgstr "職業センター"
-#: settings/serializers/security.py:183
+#: settings/serializers/security.py:180
msgid "Allow user run batch command or not using ansible"
msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない"
-#: settings/serializers/security.py:186
+#: settings/serializers/security.py:183
msgid "Session share"
msgstr "セッション共有"
-#: settings/serializers/security.py:187
+#: settings/serializers/security.py:184
msgid "Enabled, Allows user active session to be shared with other users"
msgstr ""
"ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。"
-#: settings/serializers/security.py:190
+#: settings/serializers/security.py:187
msgid "Remote Login Protection"
msgstr "リモートログイン保護"
-#: settings/serializers/security.py:192
+#: settings/serializers/security.py:189
msgid ""
"The system determines whether the login IP address belongs to a common login "
"city. If the account is logged in from a common login city, the system sends "
@@ -5037,11 +5140,11 @@ msgstr ""
"ヒント: Luna ページでグラフィック アセットを接続するときに使用するデフォルト"
"の解像度"
-#: settings/tasks/ldap.py:24
+#: settings/tasks/ldap.py:25
msgid "Import ldap user"
msgstr "LDAP ユーザーのインポート"
-#: settings/tasks/ldap.py:45
+#: settings/tasks/ldap.py:47
msgid "Periodic import ldap user"
msgstr "LDAP ユーザーを定期的にインポートする"
@@ -5424,7 +5527,7 @@ msgid "Output"
msgstr "出力"
#: terminal/backends/command/models.py:24 terminal/models/session/replay.py:9
-#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:77
+#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:81
#: terminal/templates/terminal/_msg_command_alert.html:10
#: tickets/models/ticket/command_confirm.py:15
msgid "Session"
@@ -5465,6 +5568,14 @@ msgstr "一致しない"
msgid "Tunnel"
msgstr ""
+#: terminal/const.py:71
+msgid "Read Only"
+msgstr "読み取り専用"
+
+#: terminal/const.py:72
+msgid "Writable"
+msgstr "書き込み可能"
+
#: terminal/exceptions.py:8
msgid "Bulk create not support"
msgstr "一括作成非サポート"
@@ -5489,7 +5600,7 @@ msgstr "ホスト"
msgid "Applet pkg not valid, Missing file {}"
msgstr "無効なアプレット パッケージ、ファイル {} がありません"
-#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:34
+#: terminal/models/applet/applet.py:158 terminal/models/applet/host.py:34
#: terminal/models/applet/host.py:106
msgid "Hosting"
msgstr "ホスト マシン"
@@ -5547,7 +5658,7 @@ msgid "Redis port"
msgstr "Redis ポート"
#: terminal/models/component/endpoint.py:29
-#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64
+#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:66
#: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50
#: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90
#: terminal/serializers/storage.py:98
@@ -5562,31 +5673,31 @@ msgstr "IP グループ"
msgid "Endpoint rule"
msgstr "エンドポイントルール"
-#: terminal/models/component/status.py:14
+#: terminal/models/component/status.py:15
msgid "Session Online"
msgstr "セッションオンライン"
-#: terminal/models/component/status.py:15
+#: terminal/models/component/status.py:16
msgid "CPU Load"
msgstr "CPUロード"
-#: terminal/models/component/status.py:16
+#: terminal/models/component/status.py:17
msgid "Memory Used"
msgstr "使用メモリ"
-#: terminal/models/component/status.py:17
+#: terminal/models/component/status.py:18
msgid "Disk Used"
msgstr "使用済みディスク"
-#: terminal/models/component/status.py:18
+#: terminal/models/component/status.py:19
msgid "Connections"
msgstr "接続"
-#: terminal/models/component/status.py:19
+#: terminal/models/component/status.py:20
msgid "Threads"
msgstr "スレッド"
-#: terminal/models/component/status.py:20
+#: terminal/models/component/status.py:21
msgid "Boot Time"
msgstr "ブート時間"
@@ -5595,28 +5706,28 @@ msgid "Default storage"
msgstr "デフォルトのストレージ"
#: terminal/models/component/storage.py:140
-#: terminal/models/component/terminal.py:85
+#: terminal/models/component/terminal.py:91
msgid "Command storage"
msgstr "コマンドストレージ"
#: terminal/models/component/storage.py:200
-#: terminal/models/component/terminal.py:86
+#: terminal/models/component/terminal.py:92
msgid "Replay storage"
msgstr "再生ストレージ"
-#: terminal/models/component/terminal.py:82
+#: terminal/models/component/terminal.py:88
msgid "type"
msgstr "タイプ"
-#: terminal/models/component/terminal.py:84 terminal/serializers/command.py:51
+#: terminal/models/component/terminal.py:90 terminal/serializers/command.py:51
msgid "Remote Address"
msgstr "リモートアドレス"
-#: terminal/models/component/terminal.py:87
+#: terminal/models/component/terminal.py:93
msgid "Application User"
msgstr "ユーザーの適用"
-#: terminal/models/component/terminal.py:158
+#: terminal/models/component/terminal.py:165
msgid "Can view terminal config"
msgstr "ターミナル構成を表示できます"
@@ -5640,7 +5751,7 @@ msgstr "セッション再生をダウンロードできます"
msgid "Account id"
msgstr "アカウント ID"
-#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:100
+#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:104
msgid "Login from"
msgstr "ログイン元"
@@ -5676,43 +5787,48 @@ msgstr "セッションアクションのパーマを検証できます"
msgid "Expired time (min)"
msgstr "期限切れ時間 (分)"
-#: terminal/models/session/sharing.py:36 terminal/models/session/sharing.py:82
+#: terminal/models/session/sharing.py:35 terminal/serializers/sharing.py:20
+#: terminal/serializers/sharing.py:52
+msgid "Action permission"
+msgstr "アクションパーミッション"
+
+#: terminal/models/session/sharing.py:40 terminal/models/session/sharing.py:86
msgid "Session sharing"
msgstr "セッション共有"
-#: terminal/models/session/sharing.py:38
+#: terminal/models/session/sharing.py:42
msgid "Can add super session sharing"
msgstr "スーパーセッション共有を追加できます"
-#: terminal/models/session/sharing.py:65
+#: terminal/models/session/sharing.py:69
msgid "Link not active"
msgstr "リンクがアクティブでない"
-#: terminal/models/session/sharing.py:67
+#: terminal/models/session/sharing.py:71
msgid "Link expired"
msgstr "リンク期限切れ"
-#: terminal/models/session/sharing.py:69
+#: terminal/models/session/sharing.py:73
msgid "User not allowed to join"
msgstr "ユーザーはセッションに参加できません"
-#: terminal/models/session/sharing.py:86 terminal/serializers/sharing.py:59
+#: terminal/models/session/sharing.py:90 terminal/serializers/sharing.py:71
msgid "Joiner"
msgstr "ジョイナー"
-#: terminal/models/session/sharing.py:89
+#: terminal/models/session/sharing.py:93
msgid "Date joined"
msgstr "参加日"
-#: terminal/models/session/sharing.py:92
+#: terminal/models/session/sharing.py:96
msgid "Date left"
msgstr "日付が残っています"
-#: terminal/models/session/sharing.py:115
+#: terminal/models/session/sharing.py:119
msgid "Session join record"
msgstr "セッション参加記録"
-#: terminal/models/session/sharing.py:131
+#: terminal/models/session/sharing.py:135
msgid "Invalid verification code"
msgstr "検証コードが無効"
@@ -5811,15 +5927,15 @@ msgstr "アカウント"
msgid "Timestamp"
msgstr "タイムスタンプ"
-#: terminal/serializers/endpoint.py:14
+#: terminal/serializers/endpoint.py:15
msgid "Oracle port"
msgstr "Oracle ポート"
-#: terminal/serializers/endpoint.py:17
+#: terminal/serializers/endpoint.py:18
msgid "Oracle port range"
msgstr "Oracle がリッスンするポート範囲"
-#: terminal/serializers/endpoint.py:19
+#: terminal/serializers/endpoint.py:20
msgid ""
"Oracle proxy server listen port is dynamic, Each additional Oracle database "
"instance adds a port listener"
@@ -5827,13 +5943,13 @@ msgstr ""
"Oracle プロキシサーバーがリッスンするポートは動的です。追加の Oracle データ"
"ベースインスタンスはポートリスナーを追加します"
-#: terminal/serializers/endpoint.py:35
+#: terminal/serializers/endpoint.py:36
msgid "Visit IP/Host, if empty, use the current request instead"
msgstr ""
"IP/ホストにアクセスします。空の場合は、代わりに現在のリクエストのアドレスを使"
"用します"
-#: terminal/serializers/endpoint.py:58
+#: terminal/serializers/endpoint.py:59
msgid ""
"If asset IP addresses under different endpoints conflict, use asset labels"
msgstr ""
@@ -6573,7 +6689,7 @@ msgstr "MFAフォース有効化"
#: users/serializers/user.py:92
msgid "Login blocked"
-msgstr "ログインブロック"
+msgstr "ログインがロックされました"
#: users/serializers/user.py:95 users/serializers/user.py:169
msgid "Is OTP bound"
@@ -7449,3 +7565,8 @@ msgstr "究極のエディション"
#: xpack/plugins/license/models.py:85
msgid "Community edition"
msgstr "コミュニティ版"
+
+#, fuzzy
+#~| msgid "Custom user"
+#~ msgid "Custom"
+#~ msgstr "カスタムユーザー"
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 360ae7c4b..3dbd987da 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:9819889a6d8b2934b06c5b242e3f63f404997f30851919247a405f542e8a03bc
-size 113244
+oid sha256:035f9429613b541f229855a7d36c98e5f4736efce54dcd21119660dd6d89d94e
+size 114269
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index ba0a34c84..29286bd92 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: 2023-03-14 17:34+0800\n"
+"POT-Creation-Date: 2023-04-07 13:57+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler \n"
"Language-Team: JumpServer team\n"
@@ -69,6 +69,22 @@ msgstr "数据库"
msgid "Collected"
msgstr "收集"
+#: accounts/const/account.py:24 ops/const.py:45
+msgid "Skip"
+msgstr "跳过"
+
+#: accounts/const/account.py:25 audits/const.py:23 rbac/tree.py:229
+#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6
+msgid "Update"
+msgstr "更新"
+
+#: accounts/const/account.py:26
+#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53
+#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
+#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41
+msgid "Failed"
+msgstr "失败"
+
#: accounts/const/automation.py:22 rbac/tree.py:50
msgid "Push account"
msgstr "账号推送"
@@ -158,25 +174,26 @@ msgid "Only create"
msgstr "仅创建"
#: accounts/models/account.py:47
-#: accounts/models/automations/gather_account.py:14
-#: accounts/serializers/account/account.py:95
+#: accounts/models/automations/gather_account.py:16
+#: accounts/serializers/account/account.py:173
+#: accounts/serializers/account/account.py:206
#: accounts/serializers/account/gathered_account.py:10
#: accounts/serializers/automations/change_secret.py:111
#: accounts/serializers/automations/change_secret.py:131
#: acls/models/base.py:100 acls/serializers/base.py:56
-#: assets/models/asset/common.py:92 assets/models/asset/common.py:280
+#: assets/models/asset/common.py:92 assets/models/asset/common.py:268
#: assets/models/cmd_filter.py:36 assets/serializers/domain.py:19
#: assets/serializers/label.py:27 audits/models.py:48
#: authentication/models/connection_token.py:33
-#: perms/models/asset_permission.py:64 perms/serializers/permission.py:35
+#: perms/models/asset_permission.py:64 perms/serializers/permission.py:34
#: terminal/backends/command/models.py:20 terminal/models/session/session.py:32
#: terminal/notifications.py:95 terminal/serializers/command.py:17
#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:212
msgid "Asset"
msgstr "资产"
-#: accounts/models/account.py:51 accounts/serializers/account/account.py:99
-#: authentication/serializers/connect_token_secret.py:50
+#: accounts/models/account.py:51 accounts/serializers/account/account.py:178
+#: authentication/serializers/connect_token_secret.py:48
msgid "Su from"
msgstr "切换自"
@@ -185,52 +202,56 @@ msgstr "切换自"
msgid "Version"
msgstr "版本"
-#: accounts/models/account.py:55 accounts/serializers/account/account.py:96
+#: accounts/models/account.py:55 accounts/serializers/account/account.py:174
#: users/models/user.py:768
msgid "Source"
msgstr "来源"
-#: accounts/models/account.py:58
+#: accounts/models/account.py:56
+msgid "Source ID"
+msgstr "来源 ID"
+
+#: accounts/models/account.py:59
#: accounts/serializers/automations/change_secret.py:112
#: accounts/serializers/automations/change_secret.py:132
#: acls/models/base.py:102 acls/serializers/base.py:57
-#: assets/serializers/asset/common.py:131 assets/serializers/gateway.py:28
+#: assets/serializers/asset/common.py:107 assets/serializers/gateway.py:28
#: audits/models.py:49 ops/models/base.py:18
-#: perms/models/asset_permission.py:70 perms/serializers/permission.py:40
+#: perms/models/asset_permission.py:70 perms/serializers/permission.py:39
#: terminal/backends/command/models.py:21 terminal/models/session/session.py:34
#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85
msgid "Account"
msgstr "账号"
-#: accounts/models/account.py:64
+#: accounts/models/account.py:65
msgid "Can view asset account secret"
msgstr "可以查看资产账号密码"
-#: accounts/models/account.py:65
+#: accounts/models/account.py:66
msgid "Can view asset history account"
msgstr "可以查看资产历史账号"
-#: accounts/models/account.py:66
+#: accounts/models/account.py:67
msgid "Can view asset history account secret"
msgstr "可以查看资产历史账号密码"
-#: accounts/models/account.py:67
+#: accounts/models/account.py:68
msgid "Can verify account"
msgstr "可以验证账号"
-#: accounts/models/account.py:68
+#: accounts/models/account.py:69
msgid "Can push account"
msgstr "可以推送账号"
-#: accounts/models/account.py:109
+#: accounts/models/account.py:110
msgid "Account template"
msgstr "账号模版"
-#: accounts/models/account.py:114
+#: accounts/models/account.py:115
msgid "Can view asset account template secret"
msgstr "可以查看资产账号模版密码"
-#: accounts/models/account.py:115
+#: accounts/models/account.py:116
msgid "Can change asset account template secret"
msgstr "可以更改资产账号模版密码"
@@ -275,7 +296,7 @@ msgid "Trigger mode"
msgstr "触发模式"
#: accounts/models/automations/backup_account.py:97 audits/models.py:172
-#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:168
+#: terminal/models/session/sharing.py:111 xpack/plugins/cloud/models.py:168
msgid "Reason"
msgstr "原因"
@@ -327,16 +348,17 @@ msgid "Can add push account execution"
msgstr "创建推送账号执行"
#: accounts/models/automations/change_secret.py:18 accounts/models/base.py:36
-#: accounts/serializers/account/account.py:134
+#: accounts/serializers/account/account.py:383
#: accounts/serializers/account/base.py:16
#: accounts/serializers/automations/change_secret.py:46
-#: authentication/serializers/connect_token_secret.py:41
-#: authentication/serializers/connect_token_secret.py:51
+#: authentication/serializers/connect_token_secret.py:40
+#: authentication/serializers/connect_token_secret.py:49
msgid "Secret type"
msgstr "密文类型"
#: accounts/models/automations/change_secret.py:20
#: accounts/models/automations/change_secret.py:90 accounts/models/base.py:38
+#: accounts/serializers/account/base.py:19
#: authentication/models/temp_token.py:10
#: authentication/templates/authentication/_access_key_modal.html:31
#: settings/serializers/auth/radius.py:19
@@ -375,7 +397,8 @@ msgstr "开始日期"
msgid "Date finished"
msgstr "结束日期"
-#: accounts/models/automations/change_secret.py:94 assets/const/automation.py:8
+#: accounts/models/automations/change_secret.py:94
+#: accounts/serializers/account/account.py:208 assets/const/automation.py:8
#: common/const/choices.py:20
msgid "Error"
msgstr "错误"
@@ -384,15 +407,15 @@ msgstr "错误"
msgid "Change secret record"
msgstr "改密记录"
-#: accounts/models/automations/gather_account.py:12
+#: accounts/models/automations/gather_account.py:14
msgid "Present"
msgstr "存在"
-#: accounts/models/automations/gather_account.py:13
+#: accounts/models/automations/gather_account.py:15
msgid "Date last login"
msgstr "最后登录日期"
-#: accounts/models/automations/gather_account.py:15
+#: accounts/models/automations/gather_account.py:17
#: accounts/models/automations/push_account.py:15 accounts/models/base.py:34
#: acls/serializers/base.py:18 acls/serializers/base.py:49
#: assets/models/_user.py:23 audits/models.py:157 authentication/forms.py:25
@@ -405,15 +428,19 @@ msgstr "最后登录日期"
msgid "Username"
msgstr "用户名"
-#: accounts/models/automations/gather_account.py:16
+#: accounts/models/automations/gather_account.py:18
msgid "Address last login"
msgstr "最后登录地址"
-#: accounts/models/automations/gather_account.py:23
+#: accounts/models/automations/gather_account.py:44
msgid "Gather account automation"
msgstr "自动化收集账号"
-#: accounts/models/automations/gather_account.py:39
+#: accounts/models/automations/gather_account.py:56
+msgid "Is sync account"
+msgstr "是否同步账号"
+
+#: accounts/models/automations/gather_account.py:71
#: accounts/tasks/gather_accounts.py:29
msgid "Gather asset accounts"
msgstr "收集账号"
@@ -425,7 +452,7 @@ msgstr "触发方式"
#: accounts/models/automations/push_account.py:16 acls/models/base.py:81
#: acls/serializers/base.py:81 acls/serializers/login_acl.py:25
#: assets/models/cmd_filter.py:81 audits/models.py:65 audits/serializers.py:82
-#: authentication/serializers/connect_token_secret.py:109
+#: authentication/serializers/connect_token_secret.py:107
#: authentication/templates/authentication/_access_key_modal.html:34
msgid "Action"
msgstr "动作"
@@ -444,10 +471,10 @@ msgstr "账号验证"
#: assets/models/asset/common.py:90 assets/models/asset/common.py:102
#: assets/models/cmd_filter.py:21 assets/models/domain.py:18
#: assets/models/group.py:20 assets/models/label.py:18
-#: assets/models/platform.py:21 assets/models/platform.py:76
-#: assets/serializers/asset/common.py:74 assets/serializers/asset/common.py:151
-#: assets/serializers/platform.py:133
-#: authentication/serializers/connect_token_secret.py:103 ops/mixin.py:21
+#: assets/models/platform.py:13 assets/models/platform.py:65
+#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93
+#: assets/serializers/platform.py:184
+#: authentication/serializers/connect_token_secret.py:101 ops/mixin.py:21
#: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57
#: ops/models/job.py:91 ops/models/playbook.py:23 ops/serializers/job.py:19
#: orgs/models.py:69 perms/models/asset_permission.py:56 rbac/models/role.py:29
@@ -455,7 +482,7 @@ msgstr "账号验证"
#: terminal/models/applet/applet.py:27 terminal/models/component/endpoint.py:12
#: terminal/models/component/endpoint.py:90
#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15
-#: terminal/models/component/terminal.py:79 users/forms/profile.py:33
+#: terminal/models/component/terminal.py:85 users/forms/profile.py:33
#: users/models/group.py:13 users/models/user.py:717
#: xpack/plugins/cloud/models.py:28
msgid "Name"
@@ -468,7 +495,7 @@ msgstr "特权账号"
#: accounts/models/base.py:40 assets/models/asset/common.py:109
#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39
#: assets/models/label.py:22
-#: authentication/serializers/connect_token_secret.py:107
+#: authentication/serializers/connect_token_secret.py:105
#: terminal/models/applet/applet.py:32 users/serializers/user.py:162
msgid "Is active"
msgstr "激活"
@@ -511,38 +538,36 @@ msgstr ""
"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加"
"密密码"
-#: accounts/serializers/account/account.py:65
-#: assets/serializers/asset/common.py:72 settings/serializers/auth/sms.py:75
+#: accounts/serializers/account/account.py:26
+#: settings/serializers/auth/sms.py:75
msgid "Template"
msgstr "模板"
-#: accounts/serializers/account/account.py:68
-#: assets/serializers/asset/common.py:69
+#: accounts/serializers/account/account.py:29
msgid "Push now"
msgstr "立即推送"
-#: accounts/serializers/account/account.py:70
-#: accounts/serializers/account/base.py:64
-msgid "Has secret"
-msgstr "已托管密码"
+#: accounts/serializers/account/account.py:33
+msgid "Exist policy"
+msgstr "账号存在策略"
-#: accounts/serializers/account/account.py:75 applications/models.py:11
-#: assets/models/label.py:21 assets/models/platform.py:77
-#: assets/serializers/asset/common.py:127 assets/serializers/cagegory.py:8
-#: assets/serializers/platform.py:94 assets/serializers/platform.py:134
+#: accounts/serializers/account/account.py:153 applications/models.py:11
+#: assets/models/label.py:21 assets/models/platform.py:66
+#: assets/serializers/asset/common.py:103 assets/serializers/cagegory.py:8
+#: assets/serializers/platform.py:111 assets/serializers/platform.py:185
#: perms/serializers/user_permission.py:26 settings/models.py:35
#: tickets/models/ticket/apply_application.py:13
msgid "Category"
msgstr "类别"
-#: accounts/serializers/account/account.py:76
+#: accounts/serializers/account/account.py:154
#: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24
#: acls/serializers/command_acl.py:18 applications/models.py:14
#: assets/models/_user.py:50 assets/models/automations/base.py:20
-#: assets/models/cmd_filter.py:74 assets/models/platform.py:78
-#: assets/serializers/asset/common.py:128 assets/serializers/platform.py:93
-#: audits/serializers.py:48
-#: authentication/serializers/connect_token_secret.py:116 ops/models/job.py:102
+#: assets/models/cmd_filter.py:74 assets/models/platform.py:67
+#: assets/serializers/asset/common.py:104 assets/serializers/platform.py:95
+#: assets/serializers/platform.py:110 audits/serializers.py:48
+#: authentication/serializers/connect_token_secret.py:114 ops/models/job.py:102
#: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:31
#: terminal/models/component/storage.py:57
#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29
@@ -554,10 +579,72 @@ msgstr "类别"
msgid "Type"
msgstr "类型"
-#: accounts/serializers/account/account.py:91
+#: accounts/serializers/account/account.py:169
msgid "Asset not found"
msgstr "资产不存在"
+#: accounts/serializers/account/account.py:175
+#: accounts/serializers/account/base.py:64
+msgid "Has secret"
+msgstr "已托管密码"
+
+#: accounts/serializers/account/account.py:207 ops/models/celery.py:60
+#: tickets/models/comment.py:13 tickets/models/ticket/general.py:45
+#: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14
+#: tickets/serializers/ticket/ticket.py:21
+msgid "State"
+msgstr "状态"
+
+#: accounts/serializers/account/account.py:209
+msgid "Changed"
+msgstr "已修改"
+
+#: accounts/serializers/account/account.py:213
+#: accounts/serializers/automations/base.py:22
+#: assets/models/automations/base.py:19
+#: assets/serializers/automations/base.py:20 ops/models/base.py:17
+#: ops/models/job.py:104 ops/serializers/job.py:20
+#: terminal/templates/terminal/_msg_command_execute_alert.html:16
+msgid "Assets"
+msgstr "资产"
+
+#: accounts/serializers/account/account.py:284
+msgid "Account already exists"
+msgstr "账号已存在"
+
+#: accounts/serializers/account/account.py:320
+#, python-format
+msgid "Asset does not support this secret type: %s"
+msgstr "资产不支持账号类型: %s"
+
+#: accounts/serializers/account/account.py:351
+msgid "Account has exist"
+msgstr "账号已存在"
+
+#: accounts/serializers/account/account.py:393 acls/models/base.py:98
+#: acls/models/login_acl.py:13 acls/serializers/base.py:55
+#: acls/serializers/login_acl.py:21 assets/models/cmd_filter.py:24
+#: assets/models/label.py:16 audits/models.py:44 audits/models.py:63
+#: audits/models.py:141 authentication/models/connection_token.py:29
+#: authentication/models/sso_token.py:16
+#: notifications/models/notification.py:12
+#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58
+#: perms/serializers/permission.py:30 rbac/builtin.py:122
+#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19
+#: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32
+#: terminal/notifications.py:96 terminal/notifications.py:144
+#: terminal/serializers/command.py:16 tickets/models/comment.py:21
+#: users/const.py:14 users/models/user.py:911 users/models/user.py:942
+#: users/serializers/group.py:18
+msgid "User"
+msgstr "用户"
+
+#: accounts/serializers/account/account.py:394
+#: authentication/templates/authentication/_access_key_modal.html:33
+#: terminal/notifications.py:98 terminal/notifications.py:146
+msgid "Date"
+msgstr "日期"
+
#: accounts/serializers/account/backup.py:31
#: accounts/serializers/automations/base.py:36
#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:103
@@ -579,27 +666,15 @@ msgstr "当前只支持邮件发送"
msgid "Asset type"
msgstr "资产类型"
-#: accounts/serializers/account/base.py:19
-msgid "Secret/Password"
-msgstr "密钥/密码"
-
#: accounts/serializers/account/base.py:24
msgid "Key password"
msgstr "密钥密码"
-#: accounts/serializers/account/base.py:81
-#: assets/serializers/asset/common.py:301
+#: accounts/serializers/account/base.py:80
+#: assets/serializers/asset/common.py:298
msgid "Spec info"
msgstr "特殊信息"
-#: accounts/serializers/automations/base.py:22
-#: assets/models/automations/base.py:19
-#: assets/serializers/automations/base.py:20 ops/models/base.py:17
-#: ops/models/job.py:104 ops/serializers/job.py:20
-#: terminal/templates/terminal/_msg_command_execute_alert.html:16
-msgid "Assets"
-msgstr "资产"
-
#: accounts/serializers/automations/base.py:23
#: assets/models/asset/common.py:108 assets/models/automations/base.py:18
#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21
@@ -637,17 +712,11 @@ msgstr "自动化任务执行历史"
#: accounts/serializers/automations/change_secret.py:154 audits/const.py:52
#: audits/models.py:54 audits/signal_handlers/activity_log.py:33
#: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:39
-#: terminal/const.py:59 terminal/models/session/sharing.py:103
+#: terminal/const.py:59 terminal/models/session/sharing.py:107
#: tickets/views/approve.py:114
msgid "Success"
msgstr "成功"
-#: accounts/serializers/automations/change_secret.py:155 audits/const.py:53
-#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
-#: ops/const.py:58 terminal/const.py:60 xpack/plugins/cloud/const.py:41
-msgid "Failed"
-msgstr "失败"
-
#: accounts/tasks/automation.py:24
msgid "Account execute automation"
msgstr "账号执行自动化"
@@ -717,7 +786,7 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)"
#: acls/models/base.py:82 acls/serializers/base.py:75
#: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86
-#: authentication/serializers/connect_token_secret.py:82
+#: authentication/serializers/connect_token_secret.py:79
msgid "Reviewers"
msgstr "审批人"
@@ -729,24 +798,6 @@ msgstr "审批人"
msgid "Active"
msgstr "激活中"
-#: acls/models/base.py:98 acls/models/login_acl.py:13
-#: acls/serializers/base.py:55 acls/serializers/login_acl.py:21
-#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:44
-#: audits/models.py:63 audits/models.py:141
-#: authentication/models/connection_token.py:29
-#: authentication/models/sso_token.py:16
-#: notifications/models/notification.py:12
-#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58
-#: perms/serializers/permission.py:31 rbac/builtin.py:122
-#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19
-#: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32
-#: terminal/notifications.py:96 terminal/notifications.py:144
-#: terminal/serializers/command.py:16 tickets/models/comment.py:21
-#: users/const.py:14 users/models/user.py:911 users/models/user.py:942
-#: users/serializers/group.py:18
-msgid "User"
-msgstr "用户"
-
#: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
#: ops/serializers/job.py:65 terminal/const.py:67
#: terminal/models/session/session.py:43 terminal/serializers/command.py:18
@@ -774,7 +825,7 @@ msgstr "忽略大小写"
#: acls/models/command_acl.py:33 acls/models/command_acl.py:96
#: acls/serializers/command_acl.py:28
-#: authentication/serializers/connect_token_secret.py:79
+#: authentication/serializers/connect_token_secret.py:76
msgid "Command group"
msgstr "命令组"
@@ -877,7 +928,7 @@ msgstr ""
#: authentication/templates/authentication/_msg_oauth_bind.html:12
#: authentication/templates/authentication/_msg_rest_password_success.html:8
#: authentication/templates/authentication/_msg_rest_public_key_success.html:8
-#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61
+#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:62
msgid "IP"
msgstr "IP"
@@ -902,7 +953,7 @@ msgstr "应用程序"
msgid "Can match application"
msgstr "匹配应用"
-#: assets/api/asset/asset.py:143
+#: assets/api/asset/asset.py:147
msgid "Cannot create asset directly, you should create a host or other"
msgstr "不能直接创建资产, 你应该创建主机或其他资产"
@@ -976,31 +1027,35 @@ msgstr "测试网关"
msgid "Gather facts"
msgstr "收集资产信息"
-#: assets/const/category.py:11 assets/models/asset/host.py:8
+#: assets/const/category.py:10 assets/models/asset/host.py:8
#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67
#: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17
#: xpack/plugins/cloud/serializers/account_attrs.py:72
msgid "Host"
msgstr "主机"
-#: assets/const/category.py:12 assets/models/asset/device.py:8
+#: assets/const/category.py:11 assets/models/asset/device.py:8
msgid "Device"
msgstr "网络设备"
-#: assets/const/category.py:13 assets/models/asset/database.py:9
-#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:115
+#: assets/const/category.py:12 assets/models/asset/database.py:9
+#: assets/models/asset/database.py:24 assets/serializers/asset/common.py:91
msgid "Database"
msgstr "数据库"
-#: assets/const/category.py:14
+#: assets/const/category.py:13
msgid "Cloud service"
msgstr "云服务"
-#: assets/const/category.py:15 assets/models/asset/web.py:16 audits/const.py:33
+#: assets/const/category.py:14 assets/models/asset/web.py:16 audits/const.py:33
#: terminal/models/applet/applet.py:25
msgid "Web"
msgstr "Web"
+#: assets/const/category.py:15
+msgid "Custom type"
+msgstr "自定义类型"
+
#: assets/const/cloud.py:7
msgid "Public cloud"
msgstr "公有云"
@@ -1034,7 +1089,7 @@ msgstr "防火墙"
msgid "Other"
msgstr "其它"
-#: assets/const/types.py:200
+#: assets/const/types.py:214
msgid "All types"
msgstr "所有类型"
@@ -1052,7 +1107,7 @@ msgid "Basic"
msgstr "基本"
#: assets/const/web.py:61 assets/models/asset/web.py:13
-#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:40
+#: assets/serializers/asset/common.py:99 assets/serializers/platform.py:40
msgid "Script"
msgstr "脚本"
@@ -1072,7 +1127,7 @@ msgstr "SSH公钥"
#: assets/models/cmd_filter.py:88 assets/models/group.py:23
#: common/db/models.py:37 ops/models/adhoc.py:27 ops/models/job.py:110
#: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38
-#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:158
+#: terminal/models/applet/applet.py:36 terminal/models/applet/applet.py:160
#: terminal/models/applet/host.py:111 terminal/models/component/endpoint.py:24
#: terminal/models/component/endpoint.py:100
#: terminal/models/session/session.py:47 tickets/models/comment.py:32
@@ -1121,7 +1176,7 @@ msgid "Username same with user"
msgstr "用户名与用户相同"
#: assets/models/_user.py:52 authentication/models/connection_token.py:38
-#: authentication/serializers/connect_token_secret.py:104
+#: authentication/serializers/connect_token_secret.py:102
#: terminal/models/applet/applet.py:34 terminal/serializers/session.py:20
#: terminal/serializers/session.py:41 terminal/serializers/storage.py:68
msgid "Protocol"
@@ -1175,25 +1230,25 @@ msgstr "可以匹配系统用户"
msgid "Cloud"
msgstr "云服务"
-#: assets/models/asset/common.py:91 assets/models/platform.py:22
+#: assets/models/asset/common.py:91 assets/models/platform.py:14
#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68
#: xpack/plugins/cloud/serializers/account_attrs.py:73
msgid "Port"
msgstr "端口"
-#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:152
+#: assets/models/asset/common.py:103 assets/serializers/asset/common.py:129
msgid "Address"
msgstr "地址"
-#: assets/models/asset/common.py:104 assets/models/platform.py:112
-#: authentication/serializers/connect_token_secret.py:108
+#: assets/models/asset/common.py:104 assets/models/platform.py:100
+#: authentication/serializers/connect_token_secret.py:106
#: perms/serializers/user_permission.py:24
#: xpack/plugins/cloud/serializers/account_attrs.py:187
msgid "Platform"
msgstr "系统平台"
#: assets/models/asset/common.py:106 assets/models/domain.py:21
-#: authentication/serializers/connect_token_secret.py:126
+#: authentication/serializers/connect_token_secret.py:124
#: perms/serializers/user_permission.py:28
msgid "Domain"
msgstr "网域"
@@ -1202,23 +1257,40 @@ msgstr "网域"
msgid "Labels"
msgstr "标签管理"
-#: assets/models/asset/common.py:283
+#: assets/models/asset/common.py:111
+#, fuzzy
+#| msgid "Gather asset hardware info"
+msgid "Gathered info"
+msgstr "收集资产硬件信息"
+
+#: assets/models/asset/common.py:112 assets/serializers/asset/common.py:109
+#: assets/serializers/asset/common.py:179
+#, fuzzy
+#| msgid "Auto info"
+msgid "Custom info"
+msgstr "自动化信息"
+
+#: assets/models/asset/common.py:271
msgid "Can refresh asset hardware info"
msgstr "可以更新资产硬件信息"
-#: assets/models/asset/common.py:284
+#: assets/models/asset/common.py:272
msgid "Can test asset connectivity"
msgstr "可以测试资产连接性"
-#: assets/models/asset/common.py:285
+#: assets/models/asset/common.py:273
msgid "Can match asset"
msgstr "可以匹配资产"
-#: assets/models/asset/common.py:286
+#: assets/models/asset/common.py:274
msgid "Can change asset nodes"
msgstr "可以修改资产节点"
-#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:116
+#: assets/models/asset/custom.py:8
+msgid "Custom asset"
+msgstr "自定义资产"
+
+#: assets/models/asset/database.py:10 assets/serializers/asset/common.py:92
#: settings/serializers/email.py:37
msgid "Use SSL"
msgstr "使用 SSL"
@@ -1235,7 +1307,7 @@ msgstr "客户端证书"
msgid "Client key"
msgstr "客户端密钥"
-#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:117
+#: assets/models/asset/database.py:14 assets/serializers/asset/common.py:93
msgid "Allow invalid cert"
msgstr "忽略证书校验"
@@ -1243,23 +1315,23 @@ msgstr "忽略证书校验"
msgid "Autofill"
msgstr "自动代填"
-#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:120
+#: assets/models/asset/web.py:10 assets/serializers/asset/common.py:96
#: assets/serializers/platform.py:32
msgid "Username selector"
msgstr "用户名选择器"
-#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:121
+#: assets/models/asset/web.py:11 assets/serializers/asset/common.py:97
#: assets/serializers/platform.py:35
msgid "Password selector"
msgstr "密码选择器"
-#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:122
+#: assets/models/asset/web.py:12 assets/serializers/asset/common.py:98
#: assets/serializers/platform.py:38
msgid "Submit selector"
msgstr "确认按钮选择器"
#: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38
-#: assets/serializers/asset/common.py:300 rbac/tree.py:35
+#: assets/serializers/asset/common.py:297 rbac/tree.py:35
msgid "Accounts"
msgstr "账号管理"
@@ -1273,8 +1345,8 @@ msgstr "资产自动化任务"
#: assets/models/automations/base.py:112 audits/models.py:177
#: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:183
-#: terminal/models/applet/applet.py:157 terminal/models/applet/host.py:108
-#: terminal/models/component/status.py:27 terminal/serializers/applet.py:18
+#: terminal/models/applet/applet.py:159 terminal/models/applet/host.py:108
+#: terminal/models/component/status.py:30 terminal/serializers/applet.py:18
#: terminal/serializers/applet_host.py:103 tickets/models/ticket/general.py:283
#: tickets/serializers/super_ticket.py:13
#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:164
@@ -1299,7 +1371,7 @@ msgid "Date verified"
msgstr "校验日期"
#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61
-#: perms/serializers/permission.py:33 users/models/group.py:25
+#: perms/serializers/permission.py:32 users/models/group.py:25
#: users/models/user.py:723
msgid "User group"
msgstr "用户组"
@@ -1332,7 +1404,7 @@ msgstr "命令过滤规则"
msgid "Favorite asset"
msgstr "收藏的资产"
-#: assets/models/gateway.py:35 assets/serializers/domain.py:16
+#: assets/models/gateway.py:34 assets/serializers/domain.py:16
msgid "Gateway"
msgstr "网关"
@@ -1340,7 +1412,8 @@ msgstr "网关"
msgid "Asset group"
msgstr "资产组"
-#: assets/models/group.py:34 assets/models/platform.py:19
+#: assets/models/group.py:34 assets/models/platform.py:17
+#: assets/serializers/platform.py:97
#: xpack/plugins/cloud/providers/nutanix.py:30
msgid "Default"
msgstr "默认"
@@ -1356,14 +1429,15 @@ msgstr "系统"
#: assets/models/label.py:19 assets/models/node.py:544
#: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14
#: authentication/models/connection_token.py:26
-#: authentication/serializers/connect_token_secret.py:115
+#: authentication/serializers/connect_token_secret.py:113
#: common/serializers/common.py:80 settings/models.py:34
msgid "Value"
msgstr "值"
-#: assets/models/label.py:40 assets/serializers/asset/common.py:129
+#: assets/models/label.py:40 assets/serializers/asset/common.py:105
#: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13
-#: authentication/serializers/connect_token_secret.py:114
+#: assets/serializers/platform.py:94
+#: authentication/serializers/connect_token_secret.py:112
#: common/serializers/common.py:79 settings/serializers/sms.py:7
msgid "Label"
msgstr "标签"
@@ -1388,7 +1462,7 @@ msgstr "全称"
msgid "Parent key"
msgstr "ssh私钥"
-#: assets/models/node.py:558 perms/serializers/permission.py:36
+#: assets/models/node.py:558 perms/serializers/permission.py:35
#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96
msgid "Node"
msgstr "节点"
@@ -1397,129 +1471,147 @@ msgstr "节点"
msgid "Can match node"
msgstr "可以匹配节点"
-#: assets/models/platform.py:20
+#: assets/models/platform.py:15
+msgid "Primary"
+msgstr "主要的"
+
+#: assets/models/platform.py:16
msgid "Required"
msgstr "必须的"
-#: assets/models/platform.py:23 settings/serializers/settings.py:65
+#: assets/models/platform.py:18
+msgid "Public"
+msgstr "开放的"
+
+#: assets/models/platform.py:19 settings/serializers/settings.py:65
#: users/templates/users/reset_password.html:29
msgid "Setting"
msgstr "设置"
-#: assets/models/platform.py:42 audits/const.py:47 settings/models.py:37
+#: assets/models/platform.py:31 audits/const.py:47 settings/models.py:37
#: terminal/serializers/applet_host.py:29
msgid "Enabled"
msgstr "启用"
-#: assets/models/platform.py:43
+#: assets/models/platform.py:32
msgid "Ansible config"
msgstr "Ansible 配置"
-#: assets/models/platform.py:44 assets/serializers/platform.py:61
+#: assets/models/platform.py:33 assets/serializers/platform.py:60
msgid "Ping enabled"
msgstr "启用资产探活"
-#: assets/models/platform.py:45 assets/serializers/platform.py:62
+#: assets/models/platform.py:34 assets/serializers/platform.py:61
msgid "Ping method"
msgstr "资产探活方式"
-#: assets/models/platform.py:46 assets/models/platform.py:59
-#: assets/serializers/platform.py:63
+#: assets/models/platform.py:35 assets/models/platform.py:48
+#: assets/serializers/platform.py:62
msgid "Gather facts enabled"
msgstr "启用收集资产信息"
-#: assets/models/platform.py:47 assets/models/platform.py:61
-#: assets/serializers/platform.py:64
+#: assets/models/platform.py:36 assets/models/platform.py:50
+#: assets/serializers/platform.py:63
msgid "Gather facts method"
msgstr "收集信息方式"
-#: assets/models/platform.py:48 assets/serializers/platform.py:67
+#: assets/models/platform.py:37 assets/serializers/platform.py:66
msgid "Change secret enabled"
msgstr "启用改密"
-#: assets/models/platform.py:50 assets/serializers/platform.py:68
+#: assets/models/platform.py:39 assets/serializers/platform.py:67
msgid "Change secret method"
msgstr "改密方式"
-#: assets/models/platform.py:52 assets/serializers/platform.py:69
+#: assets/models/platform.py:41 assets/serializers/platform.py:68
msgid "Push account enabled"
msgstr "启用账号推送"
-#: assets/models/platform.py:54 assets/serializers/platform.py:70
+#: assets/models/platform.py:43 assets/serializers/platform.py:69
msgid "Push account method"
msgstr "账号推送方式"
-#: assets/models/platform.py:56 assets/serializers/platform.py:65
+#: assets/models/platform.py:45 assets/serializers/platform.py:64
msgid "Verify account enabled"
msgstr "开启账号验证"
-#: assets/models/platform.py:58 assets/serializers/platform.py:66
+#: assets/models/platform.py:47 assets/serializers/platform.py:65
msgid "Verify account method"
msgstr "账号验证方式"
-#: assets/models/platform.py:79 tickets/models/ticket/general.py:300
+#: assets/models/platform.py:68 tickets/models/ticket/general.py:300
msgid "Meta"
msgstr "元数据"
-#: assets/models/platform.py:80
+#: assets/models/platform.py:69
msgid "Internal"
msgstr "内置"
-#: assets/models/platform.py:83 assets/serializers/platform.py:91
+#: assets/models/platform.py:73 assets/serializers/platform.py:109
msgid "Charset"
msgstr "编码"
-#: assets/models/platform.py:85 assets/serializers/platform.py:119
+#: assets/models/platform.py:75 assets/serializers/platform.py:133
msgid "Domain enabled"
msgstr "启用网域"
-#: assets/models/platform.py:87 assets/serializers/platform.py:118
+#: assets/models/platform.py:77 assets/serializers/platform.py:132
msgid "Su enabled"
msgstr "启用账号切换"
-#: assets/models/platform.py:88 assets/serializers/platform.py:101
+#: assets/models/platform.py:78 assets/serializers/platform.py:115
msgid "Su method"
msgstr "账号切换方式"
-#: assets/models/platform.py:90 assets/serializers/platform.py:98
+#: assets/models/platform.py:81 assets/serializers/platform.py:113
msgid "Automation"
msgstr "自动化"
+#: assets/models/platform.py:83 assets/serializers/platform.py:118
+msgid "Custom fields"
+msgstr "自定义属性"
+
#: assets/models/utils.py:18
#, python-format
msgid "%(value)s is not an even number"
msgstr "%(value)s is not an even number"
-#: assets/serializers/asset/common.py:119
+#: assets/notifications.py:12
+msgid ""
+"Batch update platform in assets, skipping assets that do not meet platform "
+"type"
+msgstr "资产中批量更新平台,不符合平台类型跳过的资产"
+
+#: assets/serializers/asset/common.py:95
msgid "Auto fill"
msgstr "自动代填"
-#: assets/serializers/asset/common.py:130 assets/serializers/platform.py:96
-#: authentication/serializers/connect_token_secret.py:28
-#: authentication/serializers/connect_token_secret.py:66
+#: assets/serializers/asset/common.py:106 assets/serializers/platform.py:112
+#: authentication/serializers/connect_token_secret.py:29
+#: authentication/serializers/connect_token_secret.py:63
#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99
msgid "Protocols"
msgstr "协议组"
-#: assets/serializers/asset/common.py:132
-#: assets/serializers/asset/common.py:153
+#: assets/serializers/asset/common.py:108
+#: assets/serializers/asset/common.py:130
msgid "Node path"
msgstr "节点路径"
-#: assets/serializers/asset/common.py:150
-#: assets/serializers/asset/common.py:302
+#: assets/serializers/asset/common.py:127
+#: assets/serializers/asset/common.py:299
msgid "Auto info"
msgstr "自动化信息"
-#: assets/serializers/asset/common.py:226
+#: assets/serializers/asset/common.py:220
msgid "Platform not exist"
msgstr "平台不存在"
-#: assets/serializers/asset/common.py:261
+#: assets/serializers/asset/common.py:255
msgid "port out of range (1-65535)"
msgstr "端口超出范围 (1-65535)"
-#: assets/serializers/asset/common.py:268
+#: assets/serializers/asset/common.py:262
msgid "Protocol is required: {}"
msgstr "协议是必填的: {}"
@@ -1568,7 +1660,7 @@ msgid "Disk total"
msgstr "硬盘大小"
#: assets/serializers/asset/host.py:21
-#: authentication/serializers/connect_token_secret.py:105
+#: authentication/serializers/connect_token_secret.py:103
msgid "OS"
msgstr "操作系统"
@@ -1620,26 +1712,30 @@ msgstr "SFTP 已启用"
msgid "SFTP home"
msgstr "SFTP 根路径"
-#: assets/serializers/platform.py:43
+#: assets/serializers/platform.py:42
msgid "Auth with username"
msgstr "使用用户名认证"
-#: assets/serializers/platform.py:71
+#: assets/serializers/platform.py:70
msgid "Gather accounts enabled"
msgstr "启用账号收集"
-#: assets/serializers/platform.py:72
+#: assets/serializers/platform.py:71
msgid "Gather accounts method"
msgstr "收集账号方式"
-#: assets/serializers/platform.py:78
-msgid "Primary"
-msgstr "主要的"
-
-#: assets/serializers/platform.py:120
+#: assets/serializers/platform.py:134
msgid "Default Domain"
msgstr "默认网域"
+#: assets/serializers/platform.py:143
+msgid "type is required"
+msgstr "类型 该字段是必填项。"
+
+#: assets/serializers/platform.py:173
+msgid "Protocols is required"
+msgstr "协议是必填的"
+
#: assets/signal_handlers/asset.py:26 assets/tasks/ping.py:35
msgid "Test assets connectivity "
msgstr "测试资产可连接性"
@@ -1752,11 +1848,6 @@ msgstr "下载文件"
msgid "View"
msgstr "查看"
-#: audits/const.py:23 rbac/tree.py:229 templates/_csv_import_export.html:18
-#: templates/_csv_update_modal.html:6
-msgid "Update"
-msgstr "更新"
-
#: audits/const.py:25
#: authentication/templates/authentication/_access_key_modal.html:22
#: rbac/tree.py:227
@@ -1778,7 +1869,7 @@ msgid "Change password"
msgstr "改密"
#: audits/const.py:34 settings/serializers/terminal.py:6
-#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:156
+#: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:163
#: terminal/serializers/session.py:48
msgid "Terminal"
msgstr "终端"
@@ -1817,7 +1908,7 @@ msgid "Job audit log"
msgstr "作业审计日志"
#: audits/models.py:46 audits/models.py:73 audits/models.py:144
-#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:95
+#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:99
msgid "Remote addr"
msgstr "远端地址"
@@ -2441,23 +2532,23 @@ msgstr "异地登录提醒"
msgid "binding reminder"
msgstr "绑定提醒"
-#: authentication/serializers/connect_token_secret.py:106
+#: authentication/serializers/connect_token_secret.py:104
msgid "Is builtin"
msgstr "内置的"
-#: authentication/serializers/connect_token_secret.py:110
+#: authentication/serializers/connect_token_secret.py:108
msgid "Options"
msgstr "选项"
-#: authentication/serializers/connect_token_secret.py:117
+#: authentication/serializers/connect_token_secret.py:115
msgid "Component"
msgstr "组件"
-#: authentication/serializers/connect_token_secret.py:128
+#: authentication/serializers/connect_token_secret.py:126
msgid "Expired now"
msgstr "立刻过期"
-#: authentication/serializers/connect_token_secret.py:148
+#: authentication/serializers/connect_token_secret.py:145
#: authentication/templates/authentication/_access_key_modal.html:30
#: perms/models/perm_node.py:21 users/serializers/group.py:33
msgid "ID"
@@ -2472,15 +2563,15 @@ msgid "Ticket info"
msgstr "工单信息"
#: authentication/serializers/connection_token.py:20
-#: perms/models/asset_permission.py:71 perms/serializers/permission.py:37
-#: perms/serializers/permission.py:70
+#: perms/models/asset_permission.py:71 perms/serializers/permission.py:36
+#: perms/serializers/permission.py:69
#: tickets/models/ticket/apply_application.py:28
#: tickets/models/ticket/apply_asset.py:18
msgid "Actions"
msgstr "动作"
#: authentication/serializers/connection_token.py:41
-#: perms/serializers/permission.py:39 perms/serializers/permission.py:71
+#: perms/serializers/permission.py:38 perms/serializers/permission.py:70
#: users/serializers/user.py:93 users/serializers/user.py:165
msgid "Is expired"
msgstr "已过期"
@@ -2500,8 +2591,8 @@ msgstr "邮箱"
msgid "The {} cannot be empty"
msgstr "{} 不能为空"
-#: authentication/serializers/token.py:79 perms/serializers/permission.py:38
-#: perms/serializers/permission.py:72 users/serializers/user.py:94
+#: authentication/serializers/token.py:79 perms/serializers/permission.py:37
+#: perms/serializers/permission.py:71 users/serializers/user.py:94
#: users/serializers/user.py:163
msgid "Is valid"
msgstr "是否有效"
@@ -2522,11 +2613,6 @@ msgstr "使用api key签名请求头,每个请求的头部是不一样的"
msgid "docs"
msgstr "文档"
-#: authentication/templates/authentication/_access_key_modal.html:33
-#: terminal/notifications.py:98 terminal/notifications.py:146
-msgid "Date"
-msgstr "日期"
-
#: authentication/templates/authentication/_access_key_modal.html:48
msgid "Show"
msgstr "显示"
@@ -2638,7 +2724,7 @@ msgid "request new one"
msgstr "重新申请"
#: authentication/templates/authentication/_msg_reset_password_code.html:12
-#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:79
+#: terminal/models/session/sharing.py:25 terminal/models/session/sharing.py:83
#: users/forms/profile.py:104 users/templates/users/forgot_password.html:65
msgid "Verify code"
msgstr "验证码"
@@ -2961,15 +3047,13 @@ msgstr "组织 ID"
msgid "The file content overflowed (The maximum length `{}` bytes)"
msgstr "文件内容太大 (最大长度 `{}` 字节)"
-#: common/drf/parsers/base.py:189
+#: common/drf/parsers/base.py:193
msgid "Parse file error: {}"
msgstr "解析文件错误: {}"
#: common/drf/parsers/excel.py:14
-#, fuzzy
-#| msgid "Invalid zip file"
msgid "Invalid excel file"
-msgstr "无效的 zip 文件"
+msgstr "无效的 excel 文件"
#: common/exceptions.py:15
#, python-format
@@ -3123,19 +3207,24 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
-#: common/views/mixins.py:58
+#: common/views/mixins.py:73
msgid "Export all"
msgstr "导出所有"
-#: common/views/mixins.py:60
+#: common/views/mixins.py:75
msgid "Export only selected items"
msgstr "仅导出选择项"
-#: common/views/mixins.py:65
+#: common/views/mixins.py:80
#, python-format
msgid "Export filtered: %s"
msgstr "导出搜素: %s"
+#: common/views/mixins.py:89
+#, python-format
+msgid "User %s view/export secret"
+msgstr "用户 %s 查看/导出 了密码"
+
#: jumpserver/conf.py:416
msgid "Create account successfully"
msgstr "创建账号成功"
@@ -3207,11 +3296,11 @@ msgstr "发布站内消息"
msgid "No account available"
msgstr "无可用账号"
-#: ops/ansible/inventory.py:189
+#: ops/ansible/inventory.py:209
msgid "Ansible disabled"
msgstr "Ansible 已禁用"
-#: ops/ansible/inventory.py:205
+#: ops/ansible/inventory.py:225
msgid "Skip hosts below:"
msgstr "跳过以下主机: "
@@ -3287,10 +3376,6 @@ msgstr "仅限特权账号"
msgid "Privileged First"
msgstr "特权账号优先"
-#: ops/const.py:45
-msgid "Skip"
-msgstr "跳过"
-
#: ops/const.py:50
msgid "Powershell"
msgstr "PowerShell"
@@ -3388,14 +3473,7 @@ msgstr "可以查看任务监控"
msgid "Kwargs"
msgstr "其它参数"
-#: ops/models/celery.py:60 tickets/models/comment.py:13
-#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279
-#: tickets/serializers/super_ticket.py:14
-#: tickets/serializers/ticket/ticket.py:21
-msgid "State"
-msgstr "状态"
-
-#: ops/models/celery.py:61 terminal/models/session/sharing.py:110
+#: ops/models/celery.py:61 terminal/models/session/sharing.py:114
#: tickets/const.py:25
msgid "Finished"
msgstr "结束"
@@ -3906,7 +3984,7 @@ msgid "My assets"
msgstr "我的资产"
#: rbac/tree.py:56 terminal/models/applet/applet.py:43
-#: terminal/models/applet/applet.py:154 terminal/models/applet/host.py:28
+#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:28
#: terminal/serializers/applet.py:15
msgid "Applet"
msgstr "远程应用"
@@ -4862,42 +4940,38 @@ msgid "Remember manual auth"
msgstr "保存手动输入密码"
#: settings/serializers/security.py:172
-msgid "Enable change auth secure mode"
-msgstr "启用改密安全模式"
-
-#: settings/serializers/security.py:175
msgid "Insecure command alert"
msgstr "危险命令告警"
-#: settings/serializers/security.py:178
+#: settings/serializers/security.py:175
msgid "Email recipient"
msgstr "邮件收件人"
-#: settings/serializers/security.py:179
+#: settings/serializers/security.py:176
msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割"
-#: settings/serializers/security.py:182
+#: settings/serializers/security.py:179
msgid "Operation center"
msgstr "作业中心"
-#: settings/serializers/security.py:183
+#: settings/serializers/security.py:180
msgid "Allow user run batch command or not using ansible"
msgstr "是否允许用户使用 ansible 执行批量命令"
-#: settings/serializers/security.py:186
+#: settings/serializers/security.py:183
msgid "Session share"
msgstr "会话分享"
-#: settings/serializers/security.py:187
+#: settings/serializers/security.py:184
msgid "Enabled, Allows user active session to be shared with other users"
msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作"
-#: settings/serializers/security.py:190
+#: settings/serializers/security.py:187
msgid "Remote Login Protection"
msgstr "异地登录保护"
-#: settings/serializers/security.py:192
+#: settings/serializers/security.py:189
msgid ""
"The system determines whether the login IP address belongs to a common login "
"city. If the account is logged in from a common login city, the system sends "
@@ -4975,11 +5049,11 @@ msgid ""
"Tip: Default resolution to use when connecting graphical assets in Luna pages"
msgstr "提示:在Luna 页面中连接图形化资产时默认使用的分辨率"
-#: settings/tasks/ldap.py:24
+#: settings/tasks/ldap.py:25
msgid "Import ldap user"
msgstr "导入 LDAP 用户"
-#: settings/tasks/ldap.py:45
+#: settings/tasks/ldap.py:47
msgid "Periodic import ldap user"
msgstr "周期导入 LDAP 用户"
@@ -5352,7 +5426,7 @@ msgid "Output"
msgstr "输出"
#: terminal/backends/command/models.py:24 terminal/models/session/replay.py:9
-#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:77
+#: terminal/models/session/sharing.py:18 terminal/models/session/sharing.py:81
#: terminal/templates/terminal/_msg_command_alert.html:10
#: tickets/models/ticket/command_confirm.py:15
msgid "Session"
@@ -5393,6 +5467,14 @@ msgstr "未匹配"
msgid "Tunnel"
msgstr "隧道"
+#: terminal/const.py:71
+msgid "Read Only"
+msgstr "只读"
+
+#: terminal/const.py:72
+msgid "Writable"
+msgstr "读写"
+
#: terminal/exceptions.py:8
msgid "Bulk create not support"
msgstr "不支持批量创建"
@@ -5417,7 +5499,7 @@ msgstr "主机"
msgid "Applet pkg not valid, Missing file {}"
msgstr "Applet pkg 无效,缺少文件 {}"
-#: terminal/models/applet/applet.py:156 terminal/models/applet/host.py:34
+#: terminal/models/applet/applet.py:158 terminal/models/applet/host.py:34
#: terminal/models/applet/host.py:106
msgid "Hosting"
msgstr "宿主机"
@@ -5475,7 +5557,7 @@ msgid "Redis port"
msgstr "Redis 端口"
#: terminal/models/component/endpoint.py:29
-#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64
+#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:66
#: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50
#: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90
#: terminal/serializers/storage.py:98
@@ -5490,31 +5572,31 @@ msgstr "IP 组"
msgid "Endpoint rule"
msgstr "端点规则"
-#: terminal/models/component/status.py:14
+#: terminal/models/component/status.py:15
msgid "Session Online"
msgstr "在线会话"
-#: terminal/models/component/status.py:15
+#: terminal/models/component/status.py:16
msgid "CPU Load"
msgstr "CPU负载"
-#: terminal/models/component/status.py:16
+#: terminal/models/component/status.py:17
msgid "Memory Used"
msgstr "内存使用"
-#: terminal/models/component/status.py:17
+#: terminal/models/component/status.py:18
msgid "Disk Used"
msgstr "磁盘使用"
-#: terminal/models/component/status.py:18
+#: terminal/models/component/status.py:19
msgid "Connections"
msgstr "连接数"
-#: terminal/models/component/status.py:19
+#: terminal/models/component/status.py:20
msgid "Threads"
msgstr "线程数"
-#: terminal/models/component/status.py:20
+#: terminal/models/component/status.py:21
msgid "Boot Time"
msgstr "运行时间"
@@ -5523,28 +5605,28 @@ msgid "Default storage"
msgstr "默认存储"
#: terminal/models/component/storage.py:140
-#: terminal/models/component/terminal.py:85
+#: terminal/models/component/terminal.py:91
msgid "Command storage"
msgstr "命令存储"
#: terminal/models/component/storage.py:200
-#: terminal/models/component/terminal.py:86
+#: terminal/models/component/terminal.py:92
msgid "Replay storage"
msgstr "录像存储"
-#: terminal/models/component/terminal.py:82
+#: terminal/models/component/terminal.py:88
msgid "type"
msgstr "类型"
-#: terminal/models/component/terminal.py:84 terminal/serializers/command.py:51
+#: terminal/models/component/terminal.py:90 terminal/serializers/command.py:51
msgid "Remote Address"
msgstr "远端地址"
-#: terminal/models/component/terminal.py:87
+#: terminal/models/component/terminal.py:93
msgid "Application User"
msgstr "应用用户"
-#: terminal/models/component/terminal.py:158
+#: terminal/models/component/terminal.py:165
msgid "Can view terminal config"
msgstr "可以查看终端配置"
@@ -5568,7 +5650,7 @@ msgstr "可以下载会话录像"
msgid "Account id"
msgstr "账号 ID"
-#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:100
+#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:104
msgid "Login from"
msgstr "登录来源"
@@ -5604,43 +5686,48 @@ msgstr "可以验证会话动作权限"
msgid "Expired time (min)"
msgstr "过期时间 (分)"
-#: terminal/models/session/sharing.py:36 terminal/models/session/sharing.py:82
+#: terminal/models/session/sharing.py:35 terminal/serializers/sharing.py:20
+#: terminal/serializers/sharing.py:52
+msgid "Action permission"
+msgstr "操作权限"
+
+#: terminal/models/session/sharing.py:40 terminal/models/session/sharing.py:86
msgid "Session sharing"
msgstr "会话分享"
-#: terminal/models/session/sharing.py:38
+#: terminal/models/session/sharing.py:42
msgid "Can add super session sharing"
msgstr "可以创建超级会话分享"
-#: terminal/models/session/sharing.py:65
+#: terminal/models/session/sharing.py:69
msgid "Link not active"
msgstr "链接失效"
-#: terminal/models/session/sharing.py:67
+#: terminal/models/session/sharing.py:71
msgid "Link expired"
msgstr "链接过期"
-#: terminal/models/session/sharing.py:69
+#: terminal/models/session/sharing.py:73
msgid "User not allowed to join"
msgstr "该用户无权加入会话"
-#: terminal/models/session/sharing.py:86 terminal/serializers/sharing.py:59
+#: terminal/models/session/sharing.py:90 terminal/serializers/sharing.py:71
msgid "Joiner"
msgstr "加入者"
-#: terminal/models/session/sharing.py:89
+#: terminal/models/session/sharing.py:93
msgid "Date joined"
msgstr "加入日期"
-#: terminal/models/session/sharing.py:92
+#: terminal/models/session/sharing.py:96
msgid "Date left"
msgstr "结束日期"
-#: terminal/models/session/sharing.py:115
+#: terminal/models/session/sharing.py:119
msgid "Session join record"
msgstr "会话加入记录"
-#: terminal/models/session/sharing.py:131
+#: terminal/models/session/sharing.py:135
msgid "Invalid verification code"
msgstr "验证码不正确"
@@ -5737,15 +5824,15 @@ msgstr "账号"
msgid "Timestamp"
msgstr "时间戳"
-#: terminal/serializers/endpoint.py:14
+#: terminal/serializers/endpoint.py:15
msgid "Oracle port"
msgstr "Oracle 端口"
-#: terminal/serializers/endpoint.py:17
+#: terminal/serializers/endpoint.py:18
msgid "Oracle port range"
msgstr "Oracle 端口范围"
-#: terminal/serializers/endpoint.py:19
+#: terminal/serializers/endpoint.py:20
msgid ""
"Oracle proxy server listen port is dynamic, Each additional Oracle database "
"instance adds a port listener"
@@ -5753,11 +5840,11 @@ msgstr ""
"Oracle 代理服务器监听端口是动态的,每增加一个 Oracle 数据库实例,就会增加一个"
"端口监听"
-#: terminal/serializers/endpoint.py:35
+#: terminal/serializers/endpoint.py:36
msgid "Visit IP/Host, if empty, use the current request instead"
msgstr "访问IP/Host,如果为空,则使用当前请求的地址代替"
-#: terminal/serializers/endpoint.py:58
+#: terminal/serializers/endpoint.py:59
msgid ""
"If asset IP addresses under different endpoints conflict, use asset labels"
msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现"
@@ -6489,7 +6576,7 @@ msgstr "强制 MFA"
#: users/serializers/user.py:92
msgid "Login blocked"
-msgstr "登录被阻塞"
+msgstr "登录被锁定"
#: users/serializers/user.py:95 users/serializers/user.py:169
msgid "Is OTP bound"
@@ -6504,10 +6591,8 @@ msgid "Avatar url"
msgstr "头像路径"
#: users/serializers/user.py:171
-#, fuzzy
-#| msgid "One level"
msgid "MFA level"
-msgstr "1 级"
+msgstr "MFA 级别"
#: users/serializers/user.py:277
msgid "Select users"
@@ -7351,6 +7436,11 @@ msgstr "旗舰版"
msgid "Community edition"
msgstr "社区版"
+#, fuzzy
+#~| msgid "Custom user"
+#~ msgid "Custom"
+#~ msgstr "自定义用户"
+
#~ msgid "API Server"
#~ msgstr "API 服务"
diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py
index 2dbfc44ef..2695851bb 100644
--- a/apps/ops/ansible/inventory.py
+++ b/apps/ops/ansible/inventory.py
@@ -49,12 +49,12 @@ class JMSInventory:
if gateway.password:
proxy_command_list.insert(
- 0, "sshpass -p '{}'".format(gateway.password)
+ 0, "sshpass -p {}".format(gateway.password)
)
if gateway.private_key:
proxy_command_list.append("-i {}".format(gateway.private_key_path))
- proxy_command = '-o ProxyCommand=\"{}\"'.format(
+ proxy_command = "-o ProxyCommand='{}'".format(
" ".join(proxy_command_list)
)
return {"ansible_ssh_common_args": proxy_command}
@@ -72,15 +72,14 @@ class JMSInventory:
var['ansible_ssh_private_key_file'] = account.private_key_path
return var
- def make_ssh_account_vars(self, host, asset, account, automation, protocols, platform, gateway):
+ def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway):
if not account:
host['error'] = _("No account available")
return host
- ssh_protocol_matched = list(filter(lambda x: x.name == 'ssh', protocols))
- ssh_protocol = ssh_protocol_matched[0] if ssh_protocol_matched else None
+ port = protocol.port if protocol else 22
host['ansible_host'] = asset.address
- host['ansible_port'] = ssh_protocol.port if ssh_protocol else 22
+ host['ansible_port'] = port
su_from = account.su_from
if platform.su_enabled and su_from:
@@ -97,15 +96,55 @@ class JMSInventory:
host.update(self.make_account_ansible_vars(account))
if gateway:
- host.update(self.make_proxy_command(gateway))
+ ansible_connection = host.get('ansible_connection', 'ssh')
+ if ansible_connection in ('local', 'winrm'):
+ host['gateway'] = {
+ 'address': gateway.address, 'port': gateway.port,
+ 'username': gateway.username, 'secret': gateway.password,
+ 'private_key_path': gateway.private_key_path
+ }
+ host['jms_asset']['port'] = port
+ else:
+ host.update(self.make_proxy_command(gateway))
+
+ @staticmethod
+ def get_primary_protocol(ansible_config, protocols):
+ invalid_protocol = type('protocol', (), {'name': 'null', 'port': 0})
+ ansible_connection = ansible_config.get('ansible_connection')
+ # 数值越小,优先级越高,若用户在 ansible_config 中配置了,则提高用户配置方式的优先级
+ protocol_priority = {'ssh': 10, 'winrm': 9, ansible_connection: 1}
+ protocol_sorted = sorted(protocols, key=lambda x: protocol_priority.get(x.name, 999))
+ protocol = protocol_sorted[0] if protocol_sorted else invalid_protocol
+ return protocol
+
+ @staticmethod
+ def fill_ansible_config(ansible_config, protocol):
+ if protocol.name in ('ssh', 'winrm'):
+ ansible_config['ansible_connection'] = protocol.name
+ if protocol.name == 'winrm':
+ if protocol.setting.get('use_ssl', False):
+ ansible_config['ansible_winrm_scheme'] = 'https'
+ ansible_config['ansible_winrm_transport'] = 'ssl'
+ ansible_config['ansible_winrm_server_cert_validation'] = 'ignore'
+ else:
+ ansible_config['ansible_winrm_scheme'] = 'http'
+ ansible_config['ansible_winrm_transport'] = 'plaintext'
+ return ansible_config
def asset_to_host(self, asset, account, automation, protocols, platform):
+ try:
+ ansible_config = dict(automation.ansible_config)
+ except (AttributeError, TypeError):
+ ansible_config = {}
+
+ protocol = self.get_primary_protocol(ansible_config, protocols)
+
host = {
'name': '{}'.format(asset.name.replace(' ', '_')),
'jms_asset': {
'id': str(asset.id), 'name': asset.name, 'address': asset.address,
'type': asset.type, 'category': asset.category,
- 'protocol': asset.protocol, 'port': asset.port,
+ 'protocol': protocol.name, 'port': protocol.port,
'spec_info': asset.spec_info, 'secret_info': asset.secret_info,
'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
},
@@ -118,33 +157,24 @@ class JMSInventory:
if host['jms_account'] and asset.platform.type == 'oracle':
host['jms_account']['mode'] = 'sysdba' if account.privileged else None
- try:
- ansible_config = dict(automation.ansible_config)
- except Exception as e:
- ansible_config = {}
- ansible_connection = ansible_config.get('ansible_connection', 'ssh')
+ ansible_config = self.fill_ansible_config(ansible_config, protocol)
host.update(ansible_config)
gateway = None
if not asset.is_gateway and asset.domain:
gateway = asset.domain.select_gateway()
- if ansible_connection == 'local':
- if gateway:
- host['gateway'] = {
- 'address': gateway.address, 'port': gateway.port,
- 'username': gateway.username, 'secret': gateway.password
- }
- else:
- self.make_ssh_account_vars(host, asset, account, automation, protocols, platform, gateway)
+ self.make_account_vars(
+ host, asset, account, automation, protocol, platform, gateway
+ )
return host
- def get_asset_accounts(self, asset):
- from assets.const import Connectivity
- accounts = asset.accounts.filter(is_active=True).order_by('-privileged', '-date_updated')
- accounts_connectivity_ok = list(accounts.filter(connectivity=Connectivity.OK))
- accounts_connectivity_no = list(accounts.exclude(connectivity=Connectivity.OK))
- return accounts_connectivity_ok + accounts_connectivity_no
+ def get_asset_sorted_accounts(self, asset):
+ accounts = list(asset.accounts.filter(is_active=True))
+ connectivity_score = {'ok': 2, '-': 1, 'err': 0}
+ sort_key = lambda x: (x.privileged, connectivity_score.get(x.connectivity, 0), x.date_updated)
+ accounts_sorted = sorted(accounts, key=sort_key, reverse=True)
+ return accounts_sorted
@staticmethod
def get_account_prefer(account_prefer):
@@ -163,37 +193,41 @@ class JMSInventory:
return account
def select_account(self, asset):
- accounts = self.get_asset_accounts(asset)
- if not accounts or self.account_policy == 'skip':
+ accounts = self.get_asset_sorted_accounts(asset)
+ if not accounts:
return None
- account_selected = None
- # 首先找到特权账号
- privileged_accounts = list(filter(lambda account: account.privileged, accounts))
+ refer_account = self.get_refer_account(accounts)
+ if refer_account:
+ return refer_account
- # 不同类型的账号选择,优先使用提供的名称
- refer_privileged_account = self.get_refer_account(privileged_accounts)
- if self.account_policy in ['privileged_only', 'privileged_first']:
- first_privileged = privileged_accounts[0] if privileged_accounts else None
- account_selected = refer_privileged_account or first_privileged
-
- # 此策略不管是否匹配到账号都需强制返回
- if self.account_policy == 'privileged_only':
+ account_selected = accounts[0]
+ if self.account_policy == 'skip':
+ return None
+ elif self.account_policy == 'privileged_first':
return account_selected
+ elif self.account_policy == 'privileged_only' and account_selected.privileged:
+ return account_selected
+ else:
+ return None
- if not account_selected:
- account_selected = self.get_refer_account(accounts)
-
- return account_selected or accounts[0]
+ @staticmethod
+ def set_platform_protocol_setting_to_asset(asset, platform_protocols):
+ asset_protocols = asset.protocols.all()
+ for p in asset_protocols:
+ setattr(p, 'setting', platform_protocols.get(p.name, {}))
+ return asset_protocols
def generate(self, path_dir):
hosts = []
platform_assets = self.group_by_platform(self.assets)
for platform, assets in platform_assets.items():
automation = platform.automation
-
+ platform_protocols = {
+ p['name']: p['setting'] for p in platform.protocols.values('name', 'setting')
+ }
for asset in assets:
- protocols = asset.protocols.all()
+ protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
account = self.select_account(asset)
host = self.asset_to_host(asset, account, automation, protocols, platform)
diff --git a/apps/ops/migrations/0025_auto_20230413_1531.py b/apps/ops/migrations/0025_auto_20230413_1531.py
new file mode 100644
index 000000000..0b77f1281
--- /dev/null
+++ b/apps/ops/migrations/0025_auto_20230413_1531.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.2.17 on 2023-04-13 07:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ops', '0024_alter_celerytask_date_last_publish'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='adhoc',
+ name='args',
+ field=models.CharField(default='', max_length=8192, verbose_name='Args'),
+ ),
+ migrations.AlterField(
+ model_name='historicaljob',
+ name='args',
+ field=models.CharField(blank=True, default='', max_length=8192, null=True, verbose_name='Args'),
+ ),
+ migrations.AlterField(
+ model_name='job',
+ name='args',
+ field=models.CharField(blank=True, default='', max_length=8192, null=True, verbose_name='Args'),
+ ),
+ migrations.AlterField(
+ model_name='jobexecution',
+ name='material',
+ field=models.CharField(blank=True, default='', max_length=8192, null=True, verbose_name='Material'),
+ ),
+ ]
diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py
index 8313be48b..254453a90 100644
--- a/apps/ops/models/adhoc.py
+++ b/apps/ops/models/adhoc.py
@@ -22,7 +22,7 @@ class AdHoc(JMSOrgBaseModel):
pattern = models.CharField(max_length=1024, verbose_name=_("Pattern"), default='all')
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
verbose_name=_('Module'))
- args = models.CharField(max_length=1024, default='', verbose_name=_('Args'))
+ args = models.CharField(max_length=8192, default='', verbose_name=_('Args'))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py
index ed936ddba..a49885dff 100644
--- a/apps/ops/models/job.py
+++ b/apps/ops/models/job.py
@@ -46,8 +46,9 @@ class JMSPermedInventory(JMSInventory):
self.user = user
self.assets_accounts_mapper = self.get_assets_accounts_mapper()
- def get_asset_accounts(self, asset):
- return self.assets_accounts_mapper.get(asset.id, [])
+ def get_asset_sorted_accounts(self, asset):
+ accounts = self.assets_accounts_mapper.get(asset.id, [])
+ return list(accounts)
def get_assets_accounts_mapper(self):
mapper = defaultdict(set)
@@ -91,7 +92,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
name = models.CharField(max_length=128, null=True, verbose_name=_('Name'))
instant = models.BooleanField(default=False)
- args = models.CharField(max_length=1024, default='', verbose_name=_('Args'), null=True, blank=True)
+ args = models.CharField(max_length=8192, default='', verbose_name=_('Args'), null=True, blank=True)
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell, verbose_name=_('Module'),
null=True)
chdir = models.CharField(default="", max_length=1024, verbose_name=_('Chdir'), null=True, blank=True)
@@ -191,7 +192,7 @@ class JobExecution(JMSOrgBaseModel):
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
- material = models.CharField(max_length=1024, default='', verbose_name=_('Material'), null=True, blank=True)
+ material = models.CharField(max_length=8192, default='', verbose_name=_('Material'), null=True, blank=True)
job_type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc,
verbose_name=_("Material Type"))
diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py
index 86c7ab188..b0c4906e4 100644
--- a/apps/ops/notifications.py
+++ b/apps/ops/notifications.py
@@ -6,7 +6,8 @@ from notifications.notifications import SystemMessage
from notifications.models import SystemMsgSubscription
from users.models import User
from notifications.backends import BACKEND
-from terminal.models import Status, Terminal
+from terminal.models.component.status import Status
+from terminal.models import Terminal
__all__ = ('ServerPerformanceMessage', 'ServerPerformanceCheckUtil')
diff --git a/apps/perms/api/user_permission/assets.py b/apps/perms/api/user_permission/assets.py
index f94854164..1ec27803f 100644
--- a/apps/perms/api/user_permission/assets.py
+++ b/apps/perms/api/user_permission/assets.py
@@ -4,7 +4,7 @@ from rest_framework.generics import ListAPIView
from assets.api.asset.asset import AssetFilterSet
from assets.models import Asset, Node
-from common.utils import get_logger, lazyproperty
+from common.utils import get_logger, lazyproperty, is_uuid
from perms import serializers
from perms.pagination import AllPermedAssetPagination
from perms.pagination import NodePermedAssetPagination
@@ -58,7 +58,12 @@ class UserAllPermedAssetsApi(BaseUserPermedAssetsApi):
pagination_class = AllPermedAssetPagination
def get_assets(self):
- return self.query_asset_util.get_all_assets()
+ node_id = self.request.query_params.get('node_id')
+ if is_uuid(node_id):
+ __, assets = self.query_asset_util.get_node_all_assets(node_id)
+ else:
+ assets = self.query_asset_util.get_all_assets()
+ return assets
class UserDirectPermedAssetsApi(BaseUserPermedAssetsApi):
diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py
index 27bc90d89..5fc19be29 100644
--- a/apps/perms/serializers/permission.py
+++ b/apps/perms/serializers/permission.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
#
-
-from django.db.models import Q, QuerySet
+from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
@@ -110,7 +109,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
if condition in username_secret_type_dict:
continue
account_data = {key: getattr(template, key) for key in account_attribute}
- account_data['name'] = f"{account_data['name']}-clone"
+ account_data['name'] = f"{account_data['name']}-{_('Account template')}"
need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data}))
return Account.objects.bulk_create(need_create_accounts)
@@ -119,7 +118,10 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
return
assets = self.get_all_assets(nodes, assets)
accounts = self.create_accounts(assets)
- push_accounts_to_assets_task.delay([str(account.id) for account in accounts])
+ account_ids = [str(account.id) for account in accounts]
+ slice_count = 20
+ for i in range(0, len(account_ids), slice_count):
+ push_accounts_to_assets_task.delay(account_ids[i:i + slice_count])
def validate_accounts(self, usernames: list[str]):
template_ids = []
diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py
index c9582cc11..4c175cba5 100644
--- a/apps/perms/serializers/user_permission.py
+++ b/apps/perms/serializers/user_permission.py
@@ -8,7 +8,7 @@ from rest_framework import serializers
from accounts.models import Account
from assets.const import Category, AllTypes
from assets.models import Node, Asset, Platform
-from assets.serializers.asset.common import AssetProtocolsSerializer
+from assets.serializers.asset.common import AssetProtocolsPermsSerializer
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.serializers.permission import ActionChoicesField
@@ -22,7 +22,7 @@ __all__ = [
class AssetPermedSerializer(OrgResourceModelSerializerMixin):
""" 被授权资产的数据结构 """
platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform'))
- protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
+ protocols = AssetProtocolsPermsSerializer(many=True, required=False, label=_('Protocols'))
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
domain = ObjectRelatedField(required=False, queryset=Node.objects, label=_('Domain'))
diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py
index d3d1562c0..b9b5b01be 100644
--- a/apps/perms/utils/permission.py
+++ b/apps/perms/utils/permission.py
@@ -92,7 +92,7 @@ class AssetPermissionUtil(object):
@staticmethod
def convert_to_queryset_if_need(objs_or_ids, model):
if not objs_or_ids:
- return objs_or_ids
+ return model.objects.none()
if isinstance(objs_or_ids, QuerySet) and isinstance(objs_or_ids.first(), model):
return objs_or_ids
ids = [
diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py
index 8f3511936..1dc41b037 100644
--- a/apps/settings/serializers/security.py
+++ b/apps/settings/serializers/security.py
@@ -168,9 +168,6 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri
SECURITY_LUNA_REMEMBER_AUTH = serializers.BooleanField(
label=_("Remember manual auth")
)
- CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = serializers.BooleanField(
- label=_("Enable change auth secure mode")
- )
SECURITY_INSECURE_COMMAND = serializers.BooleanField(
required=False, label=_('Insecure command alert')
)
diff --git a/apps/settings/tasks/ldap.py b/apps/settings/tasks/ldap.py
index 226a8139c..8a4658f2b 100644
--- a/apps/settings/tasks/ldap.py
+++ b/apps/settings/tasks/ldap.py
@@ -1,8 +1,9 @@
# coding: utf-8
-#
+#
from celery import shared_task
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
+from django.db import transaction
from common.utils import get_logger
from ops.celery.decorator import after_app_ready_start
@@ -22,6 +23,7 @@ def sync_ldap_user():
@shared_task(verbose_name=_('Import ldap user'))
+@transaction.atomic
def import_ldap_user():
logger.info("Start import ldap user task")
util_server = LDAPServerUtil()
diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html
index d207335d8..a477df5ba 100644
--- a/apps/templates/resource_download.html
+++ b/apps/templates/resource_download.html
@@ -21,8 +21,8 @@ p {
diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py
index 10fb55091..116aec94e 100644
--- a/apps/terminal/api/applet/applet.py
+++ b/apps/terminal/api/applet/applet.py
@@ -64,13 +64,7 @@ class DownloadUploadMixin:
if instance and not update:
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
- serializer = serializers.AppletSerializer(data=manifest, instance=instance)
- serializer.is_valid(raise_exception=True)
- save_to = default_storage.path('applets/{}'.format(name))
- if os.path.exists(save_to):
- shutil.rmtree(save_to)
- shutil.move(tmp_dir, save_to)
- serializer.save()
+ applet, serializer = Applet.install_from_dir(tmp_dir)
return Response(serializer.data, status=201)
@action(detail=True, methods=['get'])
diff --git a/apps/terminal/applets/chrome/app.py b/apps/terminal/applets/chrome/app.py
index 6a2f678b5..ced2456c8 100644
--- a/apps/terminal/applets/chrome/app.py
+++ b/apps/terminal/applets/chrome/app.py
@@ -18,6 +18,7 @@ class Command(Enum):
CLICK = 'click'
OPEN = 'open'
CODE = 'code'
+ SELECT_FRAME = 'select_frame'
def _execute_type(ele: WebElement, value: str):
@@ -53,6 +54,9 @@ class StepAction:
def execute(self, driver: webdriver.Chrome) -> bool:
if not self.target:
return True
+ if self.command == 'select_frame':
+ self._switch_iframe(driver, self.target)
+ return True
target_name, target_value = self.target.split("=", 1)
by_name = self.methods_map.get(target_name.upper(), By.NAME)
ele = driver.find_element(by=by_name, value=target_value)
@@ -74,6 +78,28 @@ class StepAction:
def _execute_command_type(self, ele, value):
ele.send_keys(value)
+ def _switch_iframe(self, driver: webdriver.Chrome, target: str):
+ """
+ driver: webdriver.Chrome
+ target: str
+ target support three format str below:
+ index=1: switch to frame by index, if index < 0, switch to default frame
+ id=xxx: switch to frame by id
+ name=xxx: switch to frame by name
+ """
+ target_name, target_value = target.split("=", 1)
+ if target_name == 'id':
+ driver.switch_to.frame(target_value)
+ elif target_name == 'index':
+ index = int(target_value)
+ if index < 0:
+ driver.switch_to.default_content()
+ else:
+ driver.switch_to.frame(index)
+ elif target_name == 'name':
+ driver.switch_to.frame(target_value)
+ else:
+ driver.switch_to.frame(target)
def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool:
try:
diff --git a/apps/terminal/applets/chrome/manifest.yml b/apps/terminal/applets/chrome/manifest.yml
index f2681a0c2..850ec3f08 100644
--- a/apps/terminal/applets/chrome/manifest.yml
+++ b/apps/terminal/applets/chrome/manifest.yml
@@ -1,6 +1,6 @@
name: chrome
display_name: Chrome Browser
-version: 0.1
+version: 0.2
comment: Chrome Browser Open URL Page Address
author: JumpServer Team
exec_type: python
diff --git a/apps/terminal/applets/chrome/test_data_example.json b/apps/terminal/applets/chrome/test_data_example.json
index fc8e00991..417b0163d 100644
--- a/apps/terminal/applets/chrome/test_data_example.json
+++ b/apps/terminal/applets/chrome/test_data_example.json
@@ -24,7 +24,7 @@
"autofill": "basic",
"username_selector": "name=username",
"password_selector": "name=password",
- "submit_selector": "id=longin_button",
+ "submit_selector": "id=login_button",
"script": []
},
"org_id": "2925D985-A435-411D-9BC4-FEA630F105D9"
diff --git a/apps/terminal/applets/dbeaver/app.py b/apps/terminal/applets/dbeaver/app.py
index f99fdd54c..39d953926 100644
--- a/apps/terminal/applets/dbeaver/app.py
+++ b/apps/terminal/applets/dbeaver/app.py
@@ -4,6 +4,9 @@ import win32api
import shutil
import subprocess
+from xml.etree import ElementTree
+from xml.sax import SAXException
+
from common import wait_pid, BaseApplication
@@ -21,16 +24,88 @@ class AppletApplication(BaseApplication):
self.port = self.asset.get_protocol_port(self.protocol)
self.db = self.asset.spec_info.db_name
self.name = '%s-%s-%s' % (self.host, self.db, int(time.time()))
+ self.app_work_path = self.get_app_work_path()
self.pid = None
self.app = None
- def launch(self):
+ @staticmethod
+ def get_app_work_path():
win_user_name = win32api.GetUserName()
+ return r'C:\Users\%s\AppData\Roaming\DBeaverData' % win_user_name
+
+ @staticmethod
+ def _read_config(config_file):
+ default_config = {}
+ if not os.path.exists(config_file):
+ return default_config
+
+ with open(config_file, 'r') as f:
+ for line in f.readlines():
+ try:
+ config_key, config_value = line.split('=')
+ except ValueError:
+ continue
+ default_config[config_key] = config_value
+ return default_config
+
+ @staticmethod
+ def _write_config(config_file, config):
+ with open(config_file, 'w')as f:
+ for key, value in config.items():
+ f.write(f'{key}={value}\n')
+
+ @staticmethod
+ def _merge_driver_xml(src_path, dest_path):
+ tree1 = ElementTree.parse(dest_path)
+ tree2 = ElementTree.parse(src_path)
+
+ for child2 in tree2.getroot():
+ found = False
+ for child1 in tree1.getroot():
+ if child1.tag == child2.tag and child1.attrib == child2.attrib:
+ found = True
+ break
+ if not found:
+ tree1.getroot().append(child2)
+ tree1.write(dest_path)
+
+ def init_driver(self):
src_driver = os.path.join(os.path.dirname(self.path), 'drivers')
- dest_driver = r'C:\Users\%s\AppData\Roaming\DBeaverData\drivers' % win_user_name
+ dest_driver = os.path.join(self.app_work_path, 'drivers')
if not os.path.exists(dest_driver):
shutil.copytree(src_driver, dest_driver, dirs_exist_ok=True)
+ def init_driver_config(self):
+ driver_yml_path = os.path.join(
+ self.app_work_path, 'workspace6', '.metadata', '.config',
+ )
+ driver_yml_file = os.path.join(driver_yml_path, 'drivers.xml')
+ try:
+ self._merge_driver_xml('./config/drivers.xml', driver_yml_file)
+ except (SAXException, FileNotFoundError):
+ os.makedirs(driver_yml_path, exist_ok=True)
+ shutil.copy('./config/drivers.xml', driver_yml_file)
+
+ def init_other_config(self):
+ config_path = os.path.join(
+ self.app_work_path, 'workspace6', '.metadata',
+ '.plugins', 'org.eclipse.core.runtime', '.settings',
+ )
+ os.makedirs(config_path, exist_ok=True)
+ config_file = os.path.join(config_path, 'org.jkiss.dbeaver.core.prefs')
+
+ config = self._read_config(config_file)
+ config['ui.auto.update.check'] = 'false'
+ config['sample.database.canceled'] = 'true'
+ config['tipOfTheDayInitializer.notFirstRun'] = 'true'
+ config['ui.show.tip.of.the.day.on.startup'] = 'false'
+ self._write_config(config_file, config)
+
+ def launch(self):
+ self.init_driver()
+ self.init_driver_config()
+ self.init_other_config()
+
def _get_exec_params(self):
driver = getattr(self, 'driver', self.protocol)
params_string = f'name={self.name}|' \
diff --git a/apps/terminal/applets/dbeaver/config/drivers.xml b/apps/terminal/applets/dbeaver/config/drivers.xml
new file mode 100644
index 000000000..2ce625a5b
--- /dev/null
+++ b/apps/terminal/applets/dbeaver/config/drivers.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/terminal/applets/navicat/README.md b/apps/terminal/applets/navicat/README.md
deleted file mode 100644
index ee18cd491..000000000
--- a/apps/terminal/applets/navicat/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-## Navicat Premium
-
-- 需要先手动导入License激活
-
diff --git a/apps/terminal/applets/navicat/app.py b/apps/terminal/applets/navicat/app.py
deleted file mode 100644
index 7c2dfd3bc..000000000
--- a/apps/terminal/applets/navicat/app.py
+++ /dev/null
@@ -1,411 +0,0 @@
-import os
-import shutil
-import time
-
-import winreg
-import win32api
-import win32con
-
-import const as c
-
-from pywinauto import Application
-from pywinauto.keyboard import send_keys
-from pywinauto.controls.uia_controls import (
- EditWrapper, ComboBoxWrapper, ButtonWrapper
-)
-
-from common import wait_pid, BaseApplication, _messageBox
-
-_default_path = r'C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe'
-
-
-class AppletApplication(BaseApplication):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.path = _default_path
- self.username = self.account.username
- self.password = self.account.secret
- self.privileged = self.account.privileged
- self.host = self.asset.address
- self.port = self.asset.get_protocol_port(self.protocol)
- self.db = self.asset.spec_info.db_name
- self.name = '%s-%s-%s' % (self.host, self.db, int(time.time()))
- self.use_ssl = self.asset.spec_info.use_ssl
- self.client_key = self.asset.secret_info.client_key
- self.client_key_path = None
- self.pid = None
- self.app = None
-
- @staticmethod
- def get_cert_path():
- win_user_name = win32api.GetUserName()
- cert_path = r'C:\Users\%s\AppData\Roaming\Navicat\certs' % win_user_name
- return cert_path
-
- def clean_up(self):
- protocols = (
- 'NavicatMARIADB', 'NavicatMONGODB', 'Navicat',
- 'NavicatORA', 'NavicatMSSQL', 'NavicatPG'
- )
- for p in protocols:
- sub_key = r'Software\PremiumSoft\%s\Servers' % p
- try:
- win32api.RegDeleteTree(winreg.HKEY_CURRENT_USER, sub_key)
- except Exception:
- pass
- cert_path = self.get_cert_path()
- shutil.rmtree(cert_path, ignore_errors=True)
-
- def gen_asset_file(self):
- if self.use_ssl and self.client_key:
- cert_path = self.get_cert_path()
- if not os.path.exists(cert_path):
- os.makedirs(cert_path, exist_ok=True)
- filepath = os.path.join(cert_path, str(int(time.time())))
- with open(filepath, 'w') as f:
- f.write(self.client_key)
- self.client_key_path = filepath
-
- @staticmethod
- def edit_regedit():
- sub_key = r'Software\PremiumSoft\NavicatPremium'
- try:
- key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, sub_key)
- # 禁止弹出欢迎页面
- winreg.SetValueEx(key, 'AlreadyShowNavicatV16WelcomeScreen', 0, winreg.REG_DWORD, 1)
- # 禁止开启自动检查更新
- winreg.SetValueEx(key, 'AutoCheckUpdate', 0, winreg.REG_DWORD, 0)
- # 禁止弹出初始化界面
- winreg.SetValueEx(key, 'ShareUsageData', 0, winreg.REG_DWORD, 0)
- except Exception as err:
- print('Launch error: %s' % err)
-
- def launch(self):
- # 清理因为异常未关闭的会话历史记录
- self.clean_up()
- # 生成资产依赖的相关文件
- self.gen_asset_file()
- # 修改注册表,达到一些配置目的
- self.edit_regedit()
-
- @staticmethod
- def _exec_commands(commands):
- for command in commands:
- pre_check = command.get('pre_check', lambda: True)
- if not pre_check():
- _messageBox('程序启动异常,请重新连接!!', 'Error', win32con.MB_DEFAULT_DESKTOP_ONLY)
- return
-
- time.sleep(0.5)
- if command['type'] == 'key':
- send_keys(' '.join(command['commands']))
- elif command['type'] == 'action':
- for f in command['commands']:
- f()
-
- def _pre_check_is_password_input(self):
- try:
- self.app.window(best_match='Connection Password')
- except Exception:
- return False
- return True
-
- def _action_ele_click(self, ele_name, conn_win=None):
- if not conn_win:
- conn_win = self.app.window(best_match='Dialog'). \
- child_window(title_re='New Connection')
- conn_win.child_window(best_match=ele_name).click()
-
- def _fill_mysql_auth_info(self):
- conn_window = self.app.window(best_match='Dialog'). \
- child_window(title_re='New Connection')
-
- name_ele = conn_window.child_window(best_match='Edit5')
- EditWrapper(name_ele.element_info).set_edit_text(self.name)
-
- host_ele = conn_window.child_window(best_match='Edit4')
- EditWrapper(host_ele.element_info).set_edit_text(self.host)
-
- port_ele = conn_window.child_window(best_match='Edit2')
- EditWrapper(port_ele.element_info).set_edit_text(self.port)
-
- username_ele = conn_window.child_window(best_match='Edit1')
- EditWrapper(username_ele.element_info).set_edit_text(self.username)
-
- def _get_mysql_commands(self):
- commands = [
- {
- 'type': 'key',
- 'commands': [
- '%f', c.DOWN, c.RIGHT, c.ENTER
- ],
- },
- {
- 'type': 'action',
- 'commands': [
- self._fill_mysql_auth_info, lambda: self._action_ele_click('Save password')
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.ENTER]
- }
- ]
- return commands
-
- def _get_mariadb_commands(self):
- commands = [
- {
- 'type': 'key',
- 'commands': [
- '%f', c.DOWN, c.RIGHT, c.DOWN * 5, c.ENTER,
- ],
- },
- {
- 'type': 'action',
- 'commands': [
- self._fill_mysql_auth_info, lambda: self._action_ele_click('Save password')
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.ENTER]
- }
- ]
- return commands
-
- def _fill_mongodb_auth_info(self):
- conn_window = self.app.window(best_match='Dialog'). \
- child_window(title_re='New Connection')
-
- auth_type_ele = conn_window.child_window(best_match='ComboBox2')
- ComboBoxWrapper(auth_type_ele.element_info).select('Password')
-
- name_ele = conn_window.child_window(best_match='Edit5')
- EditWrapper(name_ele.element_info).set_edit_text(self.name)
-
- host_ele = conn_window.child_window(best_match='Edit4')
- EditWrapper(host_ele.element_info).set_edit_text(self.host)
-
- port_ele = conn_window.child_window(best_match='Edit2')
- EditWrapper(port_ele.element_info).set_edit_text(self.port)
-
- db_ele = conn_window.child_window(best_match='Edit6')
- EditWrapper(db_ele.element_info).set_edit_text(self.db)
-
- username_ele = conn_window.child_window(best_match='Edit1')
- EditWrapper(username_ele.element_info).set_edit_text(self.username)
-
- def _get_mongodb_commands(self):
- commands = [
- {
- 'type': 'key',
- 'commands': [
- '%f', c.DOWN, c.RIGHT, c.DOWN * 6, c.ENTER,
- ],
- },
- {
- 'type': 'action',
- 'commands': [
- self._fill_mongodb_auth_info, lambda: self._action_ele_click('Save password')
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.ENTER]
- }
- ]
- if self.use_ssl:
- ssl_commands = [
- {
- 'type': 'key',
- 'commands': [c.TAB * 5, c.RIGHT * 3, c.TAB]
- },
- {
- 'type': 'action',
- 'commands': [
- lambda: self._action_ele_click('Use SSL'),
- lambda: self._action_ele_click('Use authentication'),
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.TAB, self.client_key_path]
- },
- {
- 'type': 'action',
- 'commands': [lambda: self._action_ele_click('Allow invalid host names')]
- }
- ]
- commands = commands[:2] + ssl_commands + commands[2:]
- return commands
-
- def _fill_postgresql_auth_info(self):
- conn_window = self.app.window(best_match='Dialog'). \
- child_window(title_re='New Connection')
-
- name_ele = conn_window.child_window(best_match='Edit6')
- EditWrapper(name_ele.element_info).set_edit_text(self.name)
-
- host_ele = conn_window.child_window(best_match='Edit5')
- EditWrapper(host_ele.element_info).set_edit_text(self.host)
-
- port_ele = conn_window.child_window(best_match='Edit2')
- EditWrapper(port_ele.element_info).set_edit_text(self.port)
-
- db_ele = conn_window.child_window(best_match='Edit4')
- EditWrapper(db_ele.element_info).set_edit_text(self.db)
-
- username_ele = conn_window.child_window(best_match='Edit1')
- EditWrapper(username_ele.element_info).set_edit_text(self.username)
-
- def _get_postgresql_commands(self):
- commands = [
- {
- 'type': 'key',
- 'commands': [
- '%f', c.DOWN, c.RIGHT, c.DOWN, c.ENTER,
- ],
- },
- {
- 'type': 'action',
- 'commands': [
- self._fill_postgresql_auth_info, lambda: self._action_ele_click('Save password')
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.ENTER]
- }
- ]
- return commands
-
- def _fill_sqlserver_auth_info(self):
- conn_window = self.app.window(best_match='Dialog'). \
- child_window(title_re='New Connection')
-
- name_ele = conn_window.child_window(best_match='Edit5')
- EditWrapper(name_ele.element_info).set_edit_text(self.name)
-
- host_ele = conn_window.child_window(best_match='Edit4')
- EditWrapper(host_ele.element_info).set_edit_text('%s,%s' % (self.host, self.port))
-
- db_ele = conn_window.child_window(best_match='Edit3')
- EditWrapper(db_ele.element_info).set_edit_text(self.db)
-
- username_ele = conn_window.child_window(best_match='Edit6')
- EditWrapper(username_ele.element_info).set_edit_text(self.username)
-
- def _get_sqlserver_commands(self):
- commands = [
- {
- 'type': 'key',
- 'commands': [
- '%f', c.DOWN, c.RIGHT, c.DOWN * 4, c.ENTER,
- ],
- },
- {
- 'type': 'action',
- 'commands': [
- self._fill_sqlserver_auth_info, lambda: self._action_ele_click('Save password')
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.ENTER]
- }
- ]
- return commands
-
- def _fill_oracle_auth_info(self):
- conn_window = self.app.window(best_match='Dialog'). \
- child_window(title_re='New Connection')
-
- name_ele = conn_window.child_window(best_match='Edit6')
- EditWrapper(name_ele.element_info).set_edit_text(self.name)
-
- host_ele = conn_window.child_window(best_match='Edit5')
- EditWrapper(host_ele.element_info).set_edit_text(self.host)
-
- port_ele = conn_window.child_window(best_match='Edit3')
- EditWrapper(port_ele.element_info).set_edit_text(self.port)
-
- db_ele = conn_window.child_window(best_match='Edit2')
- EditWrapper(db_ele.element_info).set_edit_text(self.db)
-
- username_ele = conn_window.child_window(best_match='Edit')
- EditWrapper(username_ele.element_info).set_edit_text(self.username)
-
- if self.privileged:
- conn_window.child_window(best_match='Advanced', control_type='TabItem').click_input()
- role_ele = conn_window.child_window(best_match='ComboBox2')
- ComboBoxWrapper(role_ele.element_info).select('SYSDBA')
-
- def _get_oracle_commands(self):
- commands = [
- {
- 'type': 'key',
- 'commands': [
- '%f', c.DOWN, c.RIGHT, c.DOWN * 2, c.ENTER,
- ],
- },
- {
- 'type': 'action',
- 'commands': [
- lambda: self._action_ele_click('Save password'), self._fill_oracle_auth_info
- ]
- },
- {
- 'type': 'key',
- 'commands': [c.ENTER]
- }
- ]
- return commands
-
- def run(self):
- self.launch()
- self.app = Application(backend='uia')
- work_dir = os.path.dirname(self.path)
- self.app.start(self.path, work_dir=work_dir)
- self.pid = self.app.process
-
- # 检测是否为试用版本
- try:
- trial_btn = self.app.top_window().child_window(
- best_match='Trial', control_type='Button'
- )
- ButtonWrapper(trial_btn.element_info).click()
- time.sleep(0.5)
- except Exception:
- pass
-
- # 根据协议获取相应操作命令
- action = getattr(self, '_get_%s_commands' % self.protocol, None)
- if action is None:
- raise ValueError('This protocol is not supported: %s' % self.protocol)
- commands = action()
- # 关闭掉桌面许可弹框
- commands.insert(0, {'type': 'key', 'commands': (c.ESC,)})
- # 登录
- commands.extend([
- {
- 'type': 'key',
- 'commands': (
- '%f', c.DOWN * 5, c.ENTER
- )
- },
- {
- 'type': 'key',
- 'commands': (self.password, c.ENTER),
- 'pre_check': self._pre_check_is_password_input
- }
- ])
- self._exec_commands(commands)
-
- def wait(self):
- try:
- wait_pid(self.pid)
- except Exception:
- pass
- finally:
- self.clean_up()
diff --git a/apps/terminal/applets/navicat/common.py b/apps/terminal/applets/navicat/common.py
deleted file mode 100644
index 62becd792..000000000
--- a/apps/terminal/applets/navicat/common.py
+++ /dev/null
@@ -1,215 +0,0 @@
-import abc
-import base64
-import json
-import locale
-import os
-import subprocess
-import sys
-import time
-from subprocess import CREATE_NO_WINDOW
-
-_blockInput = None
-_messageBox = None
-if sys.platform == 'win32':
- import ctypes
- from ctypes import wintypes
- import win32ui
-
- # import win32con
-
- _messageBox = win32ui.MessageBox
-
- _blockInput = ctypes.windll.user32.BlockInput
- _blockInput.argtypes = [wintypes.BOOL]
- _blockInput.restype = wintypes.BOOL
-
-
-def block_input():
- if _blockInput:
- _blockInput(True)
-
-
-def unblock_input():
- if _blockInput:
- _blockInput(False)
-
-
-def decode_content(content: bytes) -> str:
- for encoding_name in ['utf-8', 'gbk', 'gb2312']:
- try:
- return content.decode(encoding_name)
- except Exception as e:
- print(e)
- encoding_name = locale.getpreferredencoding()
- return content.decode(encoding_name)
-
-
-def notify_err_message(msg):
- if _messageBox:
- _messageBox(msg, 'Error')
-
-
-def check_pid_alive(pid) -> bool:
- # tasklist /fi "PID eq 508" /fo csv
- # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n'
- try:
-
- csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"],
- creationflags=CREATE_NO_WINDOW)
- content = decode_content(csv_ret)
- content_list = content.strip().split("\r\n")
- if len(content_list) != 2:
- print("check pid {} ret invalid: {}".format(pid, content))
- return False
- ret_pid = content_list[1].split(",")[1].strip('"')
- return str(pid) == ret_pid
- except Exception as e:
- print("check pid {} err: {}".format(pid, e))
- return False
-
-
-def wait_pid(pid):
- while 1:
- time.sleep(5)
- ok = check_pid_alive(pid)
- if not ok:
- print("pid {} is not alive".format(pid))
- break
-
-
-class DictObj:
- def __init__(self, in_dict: dict):
- assert isinstance(in_dict, dict)
- for key, val in in_dict.items():
- if isinstance(val, (list, tuple)):
- setattr(self, key, [DictObj(x) if isinstance(x, dict) else x for x in val])
- else:
- setattr(self, key, DictObj(val) if isinstance(val, dict) else val)
-
-
-class User(DictObj):
- id: str
- name: str
- username: str
-
-
-class Specific(DictObj):
- # web
- autofill: str
- username_selector: str
- password_selector: str
- submit_selector: str
- script: list
-
- # database
- db_name: str
- use_ssl: str
-
-
-class Secret(DictObj):
- client_key: str
-
-
-class Category(DictObj):
- value: str
- label: str
-
-
-class Protocol(DictObj):
- id: str
- name: str
- port: int
-
-
-class Asset(DictObj):
- id: str
- name: str
- address: str
- protocols: list[Protocol]
- category: Category
- spec_info: Specific
- secret_info: Secret
-
- def get_protocol_port(self, protocol):
- for item in self.protocols:
- if item.name == protocol:
- return item.port
- return None
-
-
-class LabelValue(DictObj):
- label: str
- value: str
-
-
-class Account(DictObj):
- id: str
- name: str
- username: str
- secret: str
- privileged: bool
- secret_type: LabelValue
-
-
-class Platform(DictObj):
- charset: str
- name: str
- charset: LabelValue
- type: LabelValue
-
-
-class Manifest(DictObj):
- name: str
- version: str
- path: str
- exec_type: str
- connect_type: str
- protocols: list[str]
-
-
-def get_manifest_data() -> dict:
- current_dir = os.path.dirname(__file__)
- manifest_file = os.path.join(current_dir, 'manifest.json')
- try:
- with open(manifest_file, "r", encoding='utf8') as f:
- return json.load(f)
- except Exception as e:
- print(e)
- return {}
-
-
-def read_app_manifest(app_dir) -> dict:
- main_json_file = os.path.join(app_dir, "manifest.json")
- if not os.path.exists(main_json_file):
- return {}
- with open(main_json_file, 'r', encoding='utf8') as f:
- return json.load(f)
-
-
-def convert_base64_to_dict(base64_str: str) -> dict:
- try:
- data_json = base64.decodebytes(base64_str.encode('utf-8')).decode('utf-8')
- return json.loads(data_json)
- except Exception as e:
- print(e)
- return {}
-
-
-class BaseApplication(abc.ABC):
-
- def __init__(self, *args, **kwargs):
- self.app_name = kwargs.get('app_name', '')
- self.protocol = kwargs.get('protocol', '')
- self.manifest = Manifest(kwargs.get('manifest', {}))
- self.user = User(kwargs.get('user', {}))
- self.asset = Asset(kwargs.get('asset', {}))
- self.account = Account(kwargs.get('account', {}))
- self.platform = Platform(kwargs.get('platform', {}))
-
- @abc.abstractmethod
- def run(self):
- raise NotImplementedError('run')
-
- @abc.abstractmethod
- def wait(self):
- raise NotImplementedError('wait')
diff --git a/apps/terminal/applets/navicat/const.py b/apps/terminal/applets/navicat/const.py
deleted file mode 100644
index 7c186b8cc..000000000
--- a/apps/terminal/applets/navicat/const.py
+++ /dev/null
@@ -1,8 +0,0 @@
-
-UP = '{UP}'
-LEFT = '{LEFT}'
-DOWN = '{DOWN}'
-RIGHT = '{RIGHT}'
-TAB = '{VK_TAB}'
-ESC = '{ESC}'
-ENTER = '{VK_RETURN}'
diff --git a/apps/terminal/applets/navicat/i18n.yml b/apps/terminal/applets/navicat/i18n.yml
deleted file mode 100644
index ec6427048..000000000
--- a/apps/terminal/applets/navicat/i18n.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-- zh:
- display_name: Navicat premium 16
- comment: 数据库管理软件
diff --git a/apps/terminal/applets/navicat/icon.png b/apps/terminal/applets/navicat/icon.png
deleted file mode 100644
index 10b343bf0..000000000
Binary files a/apps/terminal/applets/navicat/icon.png and /dev/null differ
diff --git a/apps/terminal/applets/navicat/main.py b/apps/terminal/applets/navicat/main.py
deleted file mode 100644
index be0ff3585..000000000
--- a/apps/terminal/applets/navicat/main.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import sys
-
-from common import (block_input, unblock_input)
-from common import convert_base64_to_dict
-from app import AppletApplication
-
-
-def main():
- base64_str = sys.argv[1]
- data = convert_base64_to_dict(base64_str)
- applet_app = AppletApplication(**data)
- block_input()
- applet_app.run()
- unblock_input()
- applet_app.wait()
-
-
-if __name__ == '__main__':
- try:
- main()
- except Exception as e:
- print(e)
diff --git a/apps/terminal/applets/navicat/manifest.yml b/apps/terminal/applets/navicat/manifest.yml
deleted file mode 100644
index 02db9576a..000000000
--- a/apps/terminal/applets/navicat/manifest.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: navicat
-display_name: Navicat premium 16
-comment: Database management software
-version: 0.1
-exec_type: python
-author: JumpServer Team
-type: general
-update_policy: always
-tags:
- - database
-protocols:
- - mysql
- - mariadb
- - postgresql
- - sqlserver
- - oracle
- - mongodb
diff --git a/apps/terminal/applets/navicat/setup.yml b/apps/terminal/applets/navicat/setup.yml
deleted file mode 100644
index 60e8d6c43..000000000
--- a/apps/terminal/applets/navicat/setup.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-type: manual # exe, zip, manual
-# 从这里下载的: https://www.navicat.com.cn/download/direct-download?product=navicat_premium_en_x64.exe
-source:
-destination: C:\Program Files\PremiumSoft\Navicat Premium 16
-program: C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe
-md5: 6c2c25fa56c75254c6bbcba043000063
diff --git a/apps/terminal/automations/deploy_applet_host/install_all.yml b/apps/terminal/automations/deploy_applet_host/install_all.yml
index bf3da06b4..befa8f5a2 100644
--- a/apps/terminal/automations/deploy_applet_host/install_all.yml
+++ b/apps/terminal/automations/deploy_applet_host/install_all.yml
@@ -4,5 +4,6 @@
tasks:
- name: Install all applets
- ansible.windows.win_shell:
- "tinkerd install all"
+ ansible.windows.win_powershell:
+ script: |
+ tinkerd install all
diff --git a/apps/terminal/automations/deploy_applet_host/install_applet.yml b/apps/terminal/automations/deploy_applet_host/install_applet.yml
index 5c216773f..de3c0fa49 100644
--- a/apps/terminal/automations/deploy_applet_host/install_applet.yml
+++ b/apps/terminal/automations/deploy_applet_host/install_applet.yml
@@ -6,6 +6,7 @@
tasks:
- name: Install applet
- ansible.windows.win_shell:
- "tinkerd install --name {{ applet_name }}"
+ ansible.windows.win_powershell:
+ script: |
+ tinkerd install --name {{ applet_name }}
when: applet_name != 'all'
diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml
index bd819a93f..b68f6e06c 100644
--- a/apps/terminal/automations/deploy_applet_host/playbook.yml
+++ b/apps/terminal/automations/deploy_applet_host/playbook.yml
@@ -143,9 +143,9 @@
dest: "{{ ansible_env.TEMP }}\\pip_packages"
- name: Install python requirements offline
- ansible.windows.win_shell: >
- pip install -r '{{ ansible_env.TEMP }}\pip_packages\pip_packages\requirements.txt'
- --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages\pip_packages'
+ ansible.windows.win_powershell:
+ script: |
+ pip install -r '{{ ansible_env.TEMP }}\pip_packages\pip_packages\requirements.txt' --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages\pip_packages'
- name: Download chromedriver (Chromium)
ansible.windows.win_get_url:
@@ -183,31 +183,20 @@
GOOGLE_DEFAULT_CLIENT_ID: ''
GOOGLE_DEFAULT_CLIENT_SECRET: ''
- - name: Download navicat161_premium_en package (navicat)
- ansible.windows.win_get_url:
- url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/navicat161_premium_en_x64.exe"
- dest: "{{ ansible_env.TEMP }}\\navicat161_premium_en_x64.exe"
- validate_certs: "{{ not IGNORE_VERIFY_CERTS }}"
-
- - name: Install navicat (navicat)
- ansible.windows.win_package:
- path: "{{ ansible_env.TEMP }}\\navicat161_premium_en_x64.exe"
- state: present
- arguments:
- - /SILENT
-
- name: Generate tinkerd component config
- ansible.windows.win_shell:
- "tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
- --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }} --ignore-verify-certs {{ IGNORE_VERIFY_CERTS }}"
+ ansible.windows.win_powershell:
+ script: |
+ tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }} --ignore-verify-certs {{ IGNORE_VERIFY_CERTS }}
- name: Install tinkerd service
- ansible.windows.win_shell:
- "tinkerd service install"
+ ansible.windows.win_powershell:
+ script: |
+ tinkerd service install
- name: Start tinkerd service
- ansible.windows.win_shell:
- "tinkerd service start"
+ ansible.windows.win_powershell:
+ script: |
+ tinkerd service start
- name: Wait Tinker api health
ansible.windows.win_uri:
@@ -220,5 +209,6 @@
delay: 5
- name: Sync all remote applets
- ansible.windows.win_shell:
- "tinkerd install all"
+ ansible.windows.win_powershell:
+ script: |
+ tinkerd install all
\ No newline at end of file
diff --git a/apps/terminal/const.py b/apps/terminal/const.py
index 40adb4e82..fd0421427 100644
--- a/apps/terminal/const.py
+++ b/apps/terminal/const.py
@@ -65,3 +65,8 @@ class SessionType(TextChoices):
normal = 'normal', _('Normal')
tunnel = 'tunnel', _('Tunnel')
command = 'command', _('Command')
+
+
+class ActionPermission(TextChoices):
+ readonly = "readonly", _('Read Only')
+ writable = "writable", _('Writable')
diff --git a/apps/terminal/migrations/0060_sessionsharing_action_permission.py b/apps/terminal/migrations/0060_sessionsharing_action_permission.py
new file mode 100644
index 000000000..1e0bcab94
--- /dev/null
+++ b/apps/terminal/migrations/0060_sessionsharing_action_permission.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.17 on 2023-04-03 06:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0059_session_account_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='sessionsharing',
+ name='action_permission',
+ field=models.CharField(default='writable', max_length=16, verbose_name='Action permission'),
+ ),
+ ]
diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py
index 22afe39e8..c6e938028 100644
--- a/apps/terminal/models/applet/applet.py
+++ b/apps/terminal/models/applet/applet.py
@@ -12,7 +12,6 @@ from rest_framework.serializers import ValidationError
from common.db.models import JMSBaseModel
from common.utils import lazyproperty, get_logger
-from jumpserver.utils import has_valid_xpack_license
logger = get_logger(__name__)
@@ -91,29 +90,55 @@ class Applet(JMSBaseModel):
return manifest
@classmethod
- def install_from_dir(cls, path):
+ def load_platform_if_need(cls, d):
+ from assets.serializers import PlatformSerializer
+
+ if not os.path.exists(os.path.join(d, 'platform.yml')):
+ return
+ try:
+ with open(os.path.join(d, 'platform.yml')) as f:
+ data = yaml.safe_load(f)
+ except Exception as e:
+ raise ValidationError({'error': _('Load platform.yml failed: {}').format(e)})
+
+ if data['category'] != 'custom':
+ raise ValidationError({'error': _('Only support custom platform')})
+
+ try:
+ tp = data['type']
+ except KeyError:
+ raise ValidationError({'error': _('Missing type in platform.yml')})
+
+ s = PlatformSerializer(data=data)
+ s.add_type_choices(tp, tp)
+ s.is_valid(raise_exception=True)
+ s.save()
+
+ @classmethod
+ def install_from_dir(cls, path, builtin=True):
from terminal.serializers import AppletSerializer
manifest = cls.validate_pkg(path)
name = manifest['name']
- if not has_valid_xpack_license() and name.lower() in ('navicat',):
- return
-
instance = cls.objects.filter(name=name).first()
serializer = AppletSerializer(instance=instance, data=manifest)
serializer.is_valid()
- serializer.save(builtin=True)
- pkg_path = default_storage.path('applets/{}'.format(name))
+ serializer.save(builtin=builtin)
+ cls.load_platform_if_need(path)
+
+ pkg_path = default_storage.path('applets/{}'.format(name))
if os.path.exists(pkg_path):
shutil.rmtree(pkg_path)
shutil.copytree(path, pkg_path)
- return instance
+ return instance, serializer
def select_host_account(self):
# 选择激活的发布机
- hosts = [item for item in self.hosts.filter(is_active=True).all()
- if item.load != 'offline']
+ hosts = [
+ host for host in self.hosts.filter(is_active=True)
+ if host.load != 'offline'
+ ]
if not hosts:
return None
diff --git a/apps/terminal/models/component/status.py b/apps/terminal/models/component/status.py
index 3da13d9f0..d273b9dac 100644
--- a/apps/terminal/models/component/status.py
+++ b/apps/terminal/models/component/status.py
@@ -1,11 +1,12 @@
import uuid
+from django.core.cache import cache
from django.db import models
+from django.forms.models import model_to_dict
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
-
logger = get_logger(__name__)
@@ -21,9 +22,44 @@ 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")
+ @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=60 * 3)
+ return self.save_to_cache()
+
+ 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
diff --git a/apps/terminal/models/component/terminal.py b/apps/terminal/models/component/terminal.py
index 406d2192a..74364e06a 100644
--- a/apps/terminal/models/component/terminal.py
+++ b/apps/terminal/models/component/terminal.py
@@ -1,6 +1,7 @@
import time
-
+import uuid
from django.conf import settings
+from django.core.cache import cache
from django.db import models
from django.utils.translation import ugettext_lazy as _
@@ -10,6 +11,7 @@ from common.utils import get_logger, lazyproperty
from orgs.utils import tmp_to_root_org
from terminal.const import TerminalType as TypeChoices
from users.models import User
+from .status import Status
from ..session import Session
logger = get_logger(__file__)
@@ -22,7 +24,7 @@ class TerminalStatusMixin:
@lazyproperty
def last_stat(self):
- return self.status_set.order_by('date_created').last()
+ return Status.get_terminal_latest_stat(self)
@lazyproperty
def load(self):
@@ -31,9 +33,12 @@ class TerminalStatusMixin:
@property
def is_alive(self):
- if not self.last_stat:
- return False
- return time.time() - self.last_stat.date_created.timestamp() < 150
+ key = self.ALIVE_KEY.format(self.id)
+ return cache.get(key, False)
+
+ def set_alive(self, ttl=60 * 3):
+ key = self.ALIVE_KEY.format(self.id)
+ cache.set(key, True, ttl)
class StorageMixin:
@@ -136,6 +141,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
if self.user:
setattr(self.user, SKIP_SIGNAL, True)
self.user.delete()
+ self.name = self.name + '_' + uuid.uuid4().hex[:8]
self.user = None
self.is_deleted = True
self.save()
diff --git a/apps/terminal/models/session/sharing.py b/apps/terminal/models/session/sharing.py
index b71cad900..a62e23a85 100644
--- a/apps/terminal/models/session/sharing.py
+++ b/apps/terminal/models/session/sharing.py
@@ -31,6 +31,10 @@ class SessionSharing(JMSBaseModel, OrgModelMixin):
)
users = models.TextField(blank=True, verbose_name=_("User"))
+ action_permission = models.CharField(
+ max_length=16, verbose_name=_('Action permission'), default='writable'
+ )
+
class Meta:
ordering = ('-date_created',)
verbose_name = _('Session sharing')
@@ -142,3 +146,7 @@ class SessionJoinRecord(JMSBaseModel, OrgModelMixin):
self.date_left = timezone.now()
self.is_finished = True
self.save()
+
+ @property
+ def action_permission(self):
+ return self.sharing.action_permission
diff --git a/apps/terminal/serializers/sharing.py b/apps/terminal/serializers/sharing.py
index faf4515ad..c7b8f2e4e 100644
--- a/apps/terminal/serializers/sharing.py
+++ b/apps/terminal/serializers/sharing.py
@@ -1,8 +1,11 @@
-from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
-from orgs.mixins.serializers import OrgResourceModelSerializerMixin
-from common.utils.random import random_string
+from rest_framework import serializers
+
+from common.serializers.fields import LabeledChoiceField
from common.utils.common import pretty_string
+from common.utils.random import random_string
+from orgs.mixins.serializers import OrgResourceModelSerializerMixin
+from ..const import ActionPermission
from ..models import SessionSharing, SessionJoinRecord
__all__ = ['SessionSharingSerializer', 'SessionJoinRecordSerializer']
@@ -12,13 +15,18 @@ class SessionSharingSerializer(OrgResourceModelSerializerMixin):
users = serializers.ListSerializer(
child=serializers.CharField(max_length=36), allow_null=True, write_only=True
)
+ action_permission = LabeledChoiceField(
+ default=ActionPermission.writable, choices=ActionPermission.choices,
+ write_only=True, label=_('Action permission')
+ )
class Meta:
model = SessionSharing
fields_mini = ['id']
fields_small = fields_mini + [
'verify_code', 'is_active', 'expired_time', 'created_by',
- 'date_created', 'date_updated', 'users', 'users_display'
+ 'date_created', 'date_updated', 'users', 'users_display',
+ 'action_permission'
]
fields_fk = ['session', 'creator']
fields = fields_small + fields_fk
@@ -40,13 +48,17 @@ class SessionSharingSerializer(OrgResourceModelSerializerMixin):
class SessionJoinRecordSerializer(OrgResourceModelSerializerMixin):
+ action_permission = LabeledChoiceField(
+ choices=ActionPermission.choices, read_only=True, label=_('Action permission')
+ )
+
class Meta:
model = SessionJoinRecord
fields_mini = ['id']
fields_small = fields_mini + [
'joiner_display', 'verify_code', 'date_joined', 'date_left',
'remote_addr', 'login_from', 'is_success', 'reason', 'is_finished',
- 'created_by', 'date_created', 'date_updated'
+ 'created_by', 'date_created', 'date_updated', 'action_permission'
]
fields_fk = ['session', 'sharing', 'joiner']
fields = fields_small + fields_fk
diff --git a/apps/terminal/utils/components.py b/apps/terminal/utils/components.py
index 59d021186..a3de26322 100644
--- a/apps/terminal/utils/components.py
+++ b/apps/terminal/utils/components.py
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
-import time
from itertools import groupby
from common.utils import get_logger
@@ -40,7 +39,7 @@ class ComputeLoadUtil:
@classmethod
def compute_load(cls, stat):
- if not stat or time.time() - stat.date_created.timestamp() > 150:
+ if not stat:
return ComponentLoad.offline
system_status_values = cls._compute_system_stat_status(stat).values()
if ComponentLoad.critical in system_status_values:
diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py
index 85cca8c2a..7502a7525 100644
--- a/apps/users/serializers/profile.py
+++ b/apps/users/serializers/profile.py
@@ -58,10 +58,11 @@ class UserUpdatePasswordSerializer(serializers.ModelSerializer):
class UserUpdateSecretKeySerializer(serializers.ModelSerializer):
new_secret_key = EncryptedField(required=True, max_length=128)
new_secret_key_again = EncryptedField(required=True, max_length=128)
+ has_secret_key = serializers.BooleanField(read_only=True, source='secret_key')
class Meta:
model = User
- fields = ['new_secret_key', 'new_secret_key_again']
+ fields = ['has_secret_key', 'new_secret_key', 'new_secret_key_again']
def validate(self, values):
new_secret_key = values.get('new_secret_key', '')
@@ -114,6 +115,7 @@ class UserProfileSerializer(UserSerializer):
MFA_LEVEL_CHOICES = (
(0, _('Disable')),
(1, _('Enable')),
+ (2, _("Force enable")),
)
public_key_comment = serializers.CharField(
source='get_public_key_comment', required=False, read_only=True, max_length=128
diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py
index 962fbcd24..aa997d27f 100644
--- a/apps/users/serializers/user.py
+++ b/apps/users/serializers/user.py
@@ -1,12 +1,16 @@
# -*- coding: utf-8 -*-
#
+import phonenumbers
+
from functools import partial
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.serializers import CommonBulkSerializerMixin
-from common.serializers.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField
+from common.serializers.fields import (
+ EncryptedField, ObjectRelatedField, LabeledChoiceField, PhoneField
+)
from common.utils import pretty_string, get_logger
from common.validators import PhoneValidator
from rbac.builtin import BuiltinRole
@@ -101,6 +105,9 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer
label=_("Password"), required=False, allow_blank=True,
allow_null=True, max_length=1024,
)
+ phone = PhoneField(
+ validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True, label=_("Phone")
+ )
custom_m2m_fields = {
"system_roles": [BuiltinRole.system_user],
"org_roles": [BuiltinRole.org_user],
@@ -167,7 +174,6 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer
"created_by": {"read_only": True, "allow_blank": True},
"role": {"default": "User"},
"is_otp_secret_key_bound": {"label": _("Is OTP bound")},
- "phone": {"validators": [PhoneValidator()]},
'mfa_level': {'label': _("MFA level")},
}
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 8ef8712ab..e01baa999 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -29,6 +29,7 @@ pycparser==2.21
cryptography==38.0.4
pycryptodome==3.15.0
pycryptodomex==3.15.0
+phonenumbers==8.13.8
gmssl==3.2.1
itsdangerous==1.1.0
pyotp==2.6.0
@@ -65,6 +66,7 @@ pyjwkest==1.4.2
jsonfield2==4.0.0.post0
geoip2==4.5.0
ipip-ipdb==1.6.1
+pywinrm==0.4.3
# Django environment
Django==3.2.17
django-bootstrap3==14.2.0
@@ -121,7 +123,7 @@ django-mysql==3.9.0
django-redis==5.2.0
python-redis-lock==3.7.0
pyOpenSSL==22.0.0
-redis==4.3.3
+redis==4.5.4
pyOpenSSL==22.0.0
pymongo==4.2.0
# Debug