Merge pull request #10196 from jumpserver/dev

v3.2.0 rc1
pull/10289/head
Jiangjie.Bai 2023-04-13 19:21:05 +08:00 committed by GitHub
commit 68351b1c39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
143 changed files with 3430 additions and 1812 deletions

View File

@ -8,4 +8,4 @@ celerybeat.pid
### Vagrant ### ### Vagrant ###
.vagrant/ .vagrant/
apps/xpack/.git apps/xpack/.git
.history/

View File

@ -3,7 +3,10 @@ name: 需求建议
about: 提出针对本项目的想法和建议 about: 提出针对本项目的想法和建议
title: "[Feature] " title: "[Feature] "
labels: 类型:需求 labels: 类型:需求
assignees: ibuler assignees:
- ibuler
- baijiangjie
--- ---

View File

@ -3,7 +3,9 @@ name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进 about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] " title: "[Bug] "
labels: 类型:bug labels: 类型:bug
assignees: wojiushixiaobai assignees:
- wojiushixiaobai
- baijiangjie
--- ---

View File

@ -3,7 +3,9 @@ name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题 about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] " title: "[Question] "
labels: 类型:提问 labels: 类型:提问
assignees: wojiushixiaobai assignees:
- wojiushixiaobai
- baijiangjie
--- ---

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ releashe
/apps/script.py /apps/script.py
data/* data/*
test.py test.py
.history/

View File

@ -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 ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \ && apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \ && 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 "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \ && echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \

View File

@ -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 ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \ && apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \ && 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 "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \ && echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \

View File

@ -54,6 +54,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/) - [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org) - [产品文档](https://docs.jumpserver.org)
- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver) - [知识库](https://kb.fit2cloud.com/categories/jumpserver)
## 案例研究 ## 案例研究

View File

@ -1,20 +1,21 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework.decorators import action 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.response import Response
from rest_framework.status import HTTP_200_OK
from accounts import serializers from accounts import serializers
from accounts.filters import AccountFilterSet from accounts.filters import AccountFilterSet
from accounts.models import Account from accounts.models import Account
from assets.models import Asset, Node 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 common.views.mixins import RecordViewLogMixin
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
__all__ = [ __all__ = [
'AccountViewSet', 'AccountSecretsViewSet', 'AccountViewSet', 'AccountSecretsViewSet',
'AccountHistoriesSecretAPI' 'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
] ]
@ -28,7 +29,7 @@ class AccountViewSet(OrgBulkModelViewSet):
rbac_perms = { rbac_perms = {
'partial_update': ['accounts.change_account'], 'partial_update': ['accounts.change_account'],
'su_from_accounts': 'accounts.view_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') @action(methods=['get'], detail=False, url_path='su-from-accounts')
@ -48,7 +49,10 @@ class AccountViewSet(OrgBulkModelViewSet):
serializer = serializers.AccountSerializer(accounts, many=True) serializer = serializers.AccountSerializer(accounts, many=True)
return Response(data=serializer.data) 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): def username_suggestions(self, request, *args, **kwargs):
asset_ids = request.query_params.get('assets') asset_ids = request.query_params.get('assets')
node_keys = request.query_params.get('keys') node_keys = request.query_params.get('keys')
@ -71,6 +75,12 @@ class AccountViewSet(OrgBulkModelViewSet):
usernames = common + others usernames = common + others
return Response(data=usernames) 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): 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): class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
model = Account.history.model model = Account.history.model
serializer_class = serializers.AccountHistorySerializer serializer_class = serializers.AccountHistorySerializer

View File

@ -31,8 +31,8 @@ class AccountsTaskCreateAPI(CreateAPIView):
else: else:
account = accounts[0] account = accounts[0]
asset = account.asset asset = account.asset
if not asset.auto_info['ansible_enabled'] or \ if not asset.auto_config['ansible_enabled'] or \
not asset.auto_info['ping_enabled']: not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError() raise NotSupportedTemporarilyError()
task = verify_accounts_connectivity_task.delay(account_ids) task = verify_accounts_connectivity_task.delay(account_ids)

View File

@ -1,13 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.const import Source
from accounts.filters import GatheredAccountFilterSet from accounts.filters import GatheredAccountFilterSet
from accounts.models import GatherAccountsAutomation from accounts.models import GatherAccountsAutomation
from accounts.models import GatheredAccount from accounts.models import GatheredAccount
@ -50,22 +48,12 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
'default': serializers.GatheredAccountSerializer, 'default': serializers.GatheredAccountSerializer,
} }
rbac_perms = { rbac_perms = {
'sync_account': 'assets.add_gatheredaccount', 'sync_accounts': 'assets.add_gatheredaccount',
} }
@action(methods=['post'], detail=True, url_path='sync') @action(methods=['post'], detail=False, url_path='sync-accounts')
def sync_account(self, request, *args, **kwargs): def sync_accounts(self, request, *args, **kwargs):
gathered_account = super().get_object() gathered_account_ids = request.data.get('gathered_account_ids')
asset = gathered_account.asset gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids)
username = gathered_account.username self.model.sync_accounts(gathered_accounts)
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
)
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)

View File

@ -18,18 +18,18 @@
- name: remove jumpserver ssh key - name: remove jumpserver ssh key
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
dest: "{{ kwargs.dest }}" dest: "{{ ssh_params.dest }}"
regexp: "{{ kwargs.regexp }}" regexp: "{{ ssh_params.regexp }}"
state: absent state: absent
when: when:
- account.secret_type == "ssh_key" - account.secret_type == "ssh_key"
- kwargs.strategy == "set_jms" - ssh_params.strategy == "set_jms"
- name: Change SSH key - name: Change SSH key
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ kwargs.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection

View File

@ -18,18 +18,18 @@
- name: remove jumpserver ssh key - name: remove jumpserver ssh key
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
dest: "{{ kwargs.dest }}" dest: "{{ ssh_params.dest }}"
regexp: "{{ kwargs.regexp }}" regexp: "{{ ssh_params.regexp }}"
state: absent state: absent
when: when:
- account.secret_type == "ssh_key" - account.secret_type == "ssh_key"
- kwargs.strategy == "set_jms" - ssh_params.strategy == "set_jms"
- name: Change SSH key - name: Change SSH key
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ kwargs.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection

View File

@ -42,7 +42,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def method_type(cls): def method_type(cls):
return AutomationTypes.change_secret return AutomationTypes.change_secret
def get_kwargs(self, account, secret, secret_type): def get_ssh_params(self, account, secret, secret_type):
kwargs = {} kwargs = {}
if secret_type != SecretType.SSH_KEY: if secret_type != SecretType.SSH_KEY:
return kwargs return kwargs
@ -76,6 +76,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
accounts = accounts.filter(id__in=self.account_ids) accounts = accounts.filter(id__in=self.account_ids)
if self.secret_type: if self.secret_type:
accounts = accounts.filter(secret_type=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 return accounts
def host_callback( def host_callback(
@ -106,6 +111,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
print(f'Windows {asset} does not support ssh key push') print(f'Windows {asset} does not support ssh key push')
return inventory_hosts return inventory_hosts
host['ssh_params'] = {}
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
secret_type = account.secret_type secret_type = account.secret_type
@ -124,7 +130,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
private_key_path = self.generate_private_key_path(new_secret, path_dir) private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret) 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'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,

View File

@ -12,6 +12,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.host_asset_mapper = {} self.host_asset_mapper = {}
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
@classmethod @classmethod
def method_type(cls): def method_type(cls):
@ -25,26 +26,38 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def filter_success_result(self, tp, result): def filter_success_result(self, tp, result):
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result) result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
return result return result
@staticmethod @staticmethod
def update_or_create_gathered_accounts(asset, result): def generate_data(asset, result):
with tmp_to_org(asset.org_id): data = []
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False) for username, info in result.items():
for username, data in result.items():
d = {'asset': asset, 'username': username, 'present': True} d = {'asset': asset, 'username': username, 'present': True}
if data.get('date'): if info.get('date'):
d['date_last_login'] = data['date'] d['date_last_login'] = info['date']
if data.get('address'): if info.get('address'):
d['address_last_login'] = data['address'][:32] d['address_last_login'] = info['address'][:32]
GatheredAccount.objects.update_or_create( 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 d in data:
username = d['username']
gathered_account, __ = GatheredAccount.objects.update_or_create(
defaults=d, asset=asset, username=username, 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): def on_host_success(self, host, result):
info = result.get('debug', {}).get('res', {}).get('info', {}) info = result.get('debug', {}).get('res', {}).get('info', {})
asset = self.host_asset_mapper.get(host) asset = self.host_asset_mapper.get(host)
if asset and info: if asset and info:
result = self.filter_success_result(asset.type, 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: else:
logger.error("Not found info".format(host)) logger.error("Not found info".format(host))

View File

@ -1,30 +1,6 @@
import os import os
import copy
from accounts.const import AutomationTypes
from assets.automations.methods import get_platform_automation_methods 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__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
automation_methods = get_platform_automation_methods(BASE_DIR) platform_automation_methods = get_platform_automation_methods(BASE_DIR)
platform_automation_methods = copy_change_secret_to_push_account(automation_methods)

View File

@ -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

View File

@ -0,0 +1,6 @@
id: push_account_mongodb
name: Push account for MongoDB
category: database
type:
- mongodb
method: push_account

View File

@ -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

View File

@ -0,0 +1,7 @@
id: push_account_mysql
name: Push account for MySQL
category: database
type:
- mysql
- mariadb
method: push_account

View File

@ -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

View File

@ -0,0 +1,6 @@
id: push_account_oracle
name: Push account for Oracle
category: database
type:
- oracle
method: push_account

View File

@ -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

View File

@ -0,0 +1,6 @@
id: push_account_postgresql
name: Push account for PostgreSQL
category: database
type:
- postgresql
method: push_account

View File

@ -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

View File

@ -0,0 +1,6 @@
id: push_account_sqlserver
name: Push account for SQLServer
category: database
type:
- sqlserver
method: push_account

View File

@ -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"

View File

@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

View File

@ -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"

View File

@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

View File

@ -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"

View File

@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'

View File

@ -31,6 +31,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
print(msg) print(msg)
return inventory_hosts return inventory_hosts
host['ssh_params'] = {}
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
secret_type = account.secret_type 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) private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret) 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'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,

View File

@ -18,3 +18,10 @@ class AliasAccount(TextChoices):
class Source(TextChoices): class Source(TextChoices):
LOCAL = 'local', _('Local') LOCAL = 'local', _('Local')
COLLECTED = 'collected', _('Collected') COLLECTED = 'collected', _('Collected')
TEMPLATE = 'template', _('Template')
class AccountInvalidPolicy(TextChoices):
SKIP = 'skip', _('Skip')
UPDATE = 'update', _('Update')
ERROR = 'error', _('Failed')

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -53,6 +53,7 @@ class Account(AbsConnectivity, BaseAccount):
version = models.IntegerField(default=0, verbose_name=_('Version')) version = models.IntegerField(default=0, verbose_name=_('Version'))
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version']) history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source')) 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: class Meta:
verbose_name = _('Account') verbose_name = _('Account')

View File

@ -1,7 +1,9 @@
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ 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 orgs.mixins.models import JMSOrgBaseModel
from .base import AccountBaseAutomation from .base import AccountBaseAutomation
@ -19,6 +21,25 @@ class GatheredAccount(JMSOrgBaseModel):
def address(self): def address(self):
return self.asset.address 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: class Meta:
verbose_name = _('Gather account automation') verbose_name = _('Gather account automation')
unique_together = [ unique_together = [
@ -31,6 +52,17 @@ class GatheredAccount(JMSOrgBaseModel):
class GatherAccountsAutomation(AccountBaseAutomation): 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): def save(self, *args, **kwargs):
self.type = AutomationTypes.gather_accounts self.type = AutomationTypes.gather_accounts
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -51,7 +51,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
def to_attr_json(self): def to_attr_json(self):
attr_json = super().to_attr_json() attr_json = super().to_attr_json()
attr_json.update({ attr_json.update({
'username': self.username 'username': self.username,
'params': self.params,
}) })
return attr_json return attr_json

View File

@ -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 django.utils.translation import ugettext_lazy as _
from rest_framework import serializers 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.models import Account, AccountTemplate
from accounts.tasks import push_accounts_to_assets_task from accounts.tasks import push_accounts_to_assets_task
from assets.const import Category, AllTypes from assets.const import Category, AllTypes
from assets.models import Asset 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.serializers.fields import ObjectRelatedField, LabeledChoiceField
from common.utils import get_logger
from .base import BaseAccountSerializer 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): class AccountCreateUpdateSerializerMixin(serializers.Serializer):
from_id = data.pop('id', None) template = serializers.PrimaryKeyRelatedField(
ret = super().to_internal_value(data) queryset=AccountTemplate.objects,
self.from_id = from_id required=False, label=_("Template"), write_only=True
return ret )
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): class Meta:
_id = self.from_id fields = ['template', 'push_now', 'params', 'on_invalid']
template = attrs.pop('template', None)
if _id and template: def __init__(self, *args, **kwargs):
account_template = AccountTemplate.objects.get(id=_id) super().__init__(*args, **kwargs)
attrs['secret'] = account_template.secret self.set_initial_value()
elif _id and not template:
account = Account.objects.get(id=_id) def set_initial_value(self):
attrs['secret'] = account.secret if not getattr(self, 'initial_data', None):
return attrs 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): def validate(self, attrs):
attrs = super().validate(attrs) attrs = super().validate(attrs)
return self.set_secret(attrs) if self.instance:
return attrs
@staticmethod template = attrs.pop('template', None)
def push_account(instance, push_now): if template:
if not push_now: attrs['source'] = Source.TEMPLATE
return attrs['source_id'] = str(template.id)
push_accounts_to_assets_task.delay([str(instance.id)]) return attrs
def create(self, validated_data): def create(self, validated_data):
push_now = validated_data.pop('push_now', None) push_now = validated_data.pop('push_now', None)
instance = super().create(validated_data) params = validated_data.pop('params', None)
self.push_account(instance, push_now) instance, stat = self.do_create(validated_data)
self.push_account_if_need(instance, push_now, params, stat)
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
# account cannot be modified # account cannot be modified
validated_data.pop('username', None) validated_data.pop('username', None)
validated_data.pop('on_invalid', None)
push_now = validated_data.pop('push_now', 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) instance = super().update(instance, validated_data)
self.push_account(instance, push_now) self.push_account_if_need(instance, push_now, params, 'updated')
return instance 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): class AccountAssetSerializer(serializers.ModelSerializer):
platform = ObjectRelatedField(read_only=True) platform = ObjectRelatedField(read_only=True)
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
@ -77,11 +174,11 @@ class AccountAssetSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset 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): def to_internal_value(self, data):
if isinstance(data, dict): if isinstance(data, dict):
i = data.get('id') i = data.get('id') or data.get('pk')
else: else:
i = data i = data
@ -91,9 +188,10 @@ class AccountAssetSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(_('Asset not found')) raise serializers.ValidationError(_('Asset not found'))
class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer): class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
asset = AccountAssetSerializer(label=_('Asset')) asset = AccountAssetSerializer(label=_('Asset'))
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True) source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
su_from = ObjectRelatedField( su_from = ObjectRelatedField(
required=False, queryset=Account.objects, allow_null=True, allow_empty=True, required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
label=_('Su from'), attrs=('id', 'name', 'username') label=_('Su from'), attrs=('id', 'name', 'username')
@ -102,27 +200,179 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
class Meta(BaseAccountSerializer.Meta): class Meta(BaseAccountSerializer.Meta):
model = Account model = Account
fields = BaseAccountSerializer.Meta.fields + [ fields = BaseAccountSerializer.Meta.fields + [
'su_from', 'asset', 'template', 'version', 'su_from', 'asset', 'version',
'push_now', 'source', 'connectivity', 'source', 'source_id', 'connectivity',
] + AccountCreateUpdateSerializerMixin.Meta.fields
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
'source', 'source_id', 'connectivity'
] ]
extra_kwargs = { extra_kwargs = {
**BaseAccountSerializer.Meta.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 @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset \ queryset = queryset.prefetch_related(
.prefetch_related('asset', 'asset__platform', 'asset__platform__automation') 'asset', 'asset__platform',
'asset__platform__automation'
)
return queryset 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 AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
extra_kwargs = { extra_kwargs = {

View File

@ -13,10 +13,10 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
class AuthValidateMixin(serializers.Serializer): class AuthValidateMixin(serializers.Serializer):
secret_type = LabeledChoiceField( secret_type = LabeledChoiceField(
choices=SecretType.choices, required=True, label=_('Secret type') choices=SecretType.choices, label=_('Secret type'), default='password'
) )
secret = EncryptedField( 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, allow_null=True, write_only=True,
) )
passphrase = serializers.CharField( passphrase = serializers.CharField(
@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
'date_verified', 'created_by', 'date_created', 'date_verified', 'created_by', 'date_created',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': True},
'spec_info': {'label': _('Spec info')}, 'spec_info': {'label': _('Spec info')},
} }

View File

@ -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 common.serializers import SecretReadableMixin
from .base import BaseAccountSerializer from .base import BaseAccountSerializer
@ -7,17 +8,47 @@ class AccountTemplateSerializer(BaseAccountSerializer):
class Meta(BaseAccountSerializer.Meta): class Meta(BaseAccountSerializer.Meta):
model = AccountTemplate model = AccountTemplate
# @classmethod @staticmethod
# def validate_required(cls, attrs): def bulk_update_accounts(instance, diff):
# # TODO 选择模版后检查一些必填项 accounts = Account.objects.filter(source_id=instance.id)
# required_field_dict = {} if not accounts:
# error = _('This field is required.') return
# for k, v in cls().fields.items():
# if v.required and k not in attrs: secret_type = diff.pop('secret_type', None)
# required_field_dict[k] = error diff.pop('secret', None)
# if not required_field_dict: update_accounts = []
# return for account in accounts:
# raise serializers.ValidationError(required_field_dict) 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): class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):

View File

@ -17,7 +17,8 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
class Meta: class Meta:
model = GatherAccountsAutomation model = GatherAccountsAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields 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 extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs

View File

@ -7,9 +7,10 @@ from .change_secret import (
class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer): class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
class Meta(ChangeSecretAutomationSerializer.Meta): class Meta(ChangeSecretAutomationSerializer.Meta):
model = PushAccountAutomation model = PushAccountAutomation
fields = [ fields = ['params'] + [
n for n in ChangeSecretAutomationSerializer.Meta.fields n for n in ChangeSecretAutomationSerializer.Meta.fields
if n not in ['recipients'] if n not in ['recipients']
] ]

View File

@ -8,8 +8,8 @@ logger = get_logger(__name__)
@receiver(pre_save, sender=Account) @receiver(pre_save, sender=Account)
def on_account_pre_save(sender, instance, created=False, **kwargs): def on_account_pre_save(sender, instance, **kwargs):
if created: if instance.version == 0:
instance.version = 1 instance.version = 1
else: else:
instance.version = instance.history.count() instance.version = instance.history.count()

View File

@ -15,7 +15,7 @@ __all__ = [
queue="ansible", verbose_name=_('Push accounts to assets'), queue="ansible", verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None) 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 PushAccountAutomation
from accounts.models import Account from accounts.models import Account
@ -26,6 +26,7 @@ def push_accounts_to_assets_task(account_ids):
task_snapshot = { task_snapshot = {
'accounts': [str(account.id) for account in accounts], 'accounts': [str(account.id) for account in accounts],
'assets': [str(account.asset_id) for account in accounts], 'assets': [str(account.asset_id) for account in accounts],
'params': params or {},
} }
tp = AutomationTypes.push_account tp = AutomationTypes.push_account

View File

@ -25,6 +25,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record') router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
urlpatterns = [ urlpatterns = [
path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),
path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'), path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(), path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
name='account-secret-history'), name='account-secret-history'),

View File

@ -1,7 +1,8 @@
from .asset import * from .asset import *
from .host import *
from .database import *
from .web import *
from .cloud import * from .cloud import *
from .custom import *
from .database import *
from .device import * from .device import *
from .host import *
from .permission import * from .permission import *
from .web import *

View File

@ -2,15 +2,17 @@
# #
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response 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 accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
from assets import serializers from assets import serializers
from assets.exceptions import NotSupportedTemporarilyError from assets.exceptions import NotSupportedTemporarilyError
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend 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 assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
from common.api import SuggestionMixin from common.api import SuggestionMixin
from common.drf.filters import BaseFilterSet 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 import generics
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from ..mixin import NodeFilterMixin from ..mixin import NodeFilterMixin
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
@ -99,16 +102,16 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
("platform", serializers.PlatformSerializer), ("platform", serializers.PlatformSerializer),
("suggestion", serializers.MiniAssetSerializer), ("suggestion", serializers.MiniAssetSerializer),
("gateways", serializers.GatewaySerializer), ("gateways", serializers.GatewaySerializer),
("spec_info", serializers.SpecSerializer),
) )
rbac_perms = ( rbac_perms = (
("match", "assets.match_asset"), ("match", "assets.match_asset"),
("platform", "assets.view_platform"), ("platform", "assets.view_platform"),
("gateways", "assets.view_gateway"), ("gateways", "assets.view_gateway"),
("spec_info", "assets.view_asset"), ("spec_info", "assets.view_asset"),
("info", "assets.view_asset"), ("gathered_info", "assets.view_asset"),
) )
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
skip_assets = []
def get_serializer_class(self): def get_serializer_class(self):
cls = super().get_serializer_class() cls = super().get_serializer_class()
@ -124,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
serializer = super().get_serializer(instance=asset.platform) serializer = super().get_serializer(instance=asset.platform)
return Response(serializer.data) 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") @action(methods=["GET"], detail=True, url_path="gateways")
def gateways(self, *args, **kwargs): def gateways(self, *args, **kwargs):
asset = self.get_object() asset = self.get_object()
@ -144,6 +142,31 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
return Response({'error': error}, status=400) return Response({'error': error}, status=400)
return super().create(request, *args, **kwargs) 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: class AssetsTaskMixin:
def perform_assets_task(self, serializer): def perform_assets_task(self, serializer):
@ -154,8 +177,8 @@ class AssetsTaskMixin:
task = update_assets_hardware_info_manual(assets) task = update_assets_hardware_info_manual(assets)
else: else:
asset = assets[0] asset = assets[0]
if not asset.auto_info['ansible_enabled'] or \ if not asset.auto_config['ansible_enabled'] or \
not asset.auto_info['ping_enabled']: not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError() raise NotSupportedTemporarilyError()
task = test_assets_connectivity_manual(assets) task = test_assets_connectivity_manual(assets)
return task return task

View File

@ -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

View File

@ -1,8 +1,5 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.models import Host, Asset from assets.models import Host, Asset
from assets.serializers import HostSerializer, HostInfoSerializer from assets.serializers import HostSerializer
from .asset import AssetViewSet from .asset import AssetViewSet
__all__ = ['HostViewSet'] __all__ = ['HostViewSet']
@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet):
def get_serializer_classes(self): def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes() serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = HostSerializer serializer_classes['default'] = HostSerializer
serializer_classes['info'] = HostInfoSerializer
return serializer_classes 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)

View File

@ -8,6 +8,15 @@ from common.utils import lazyproperty, timeit
class SerializeToTreeNodeMixin: 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 @timeit
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False): def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
@ -17,6 +26,16 @@ class SerializeToTreeNodeMixin:
else: else:
def _name(node: Node): def _name(node: Node):
return node.value return node.value
def _open(node):
if not self.is_sync:
# 异步加载资产树时,默认展开节点
return True
if not node.parent_key:
return True
else:
return False
data = [ data = [
{ {
'id': node.key, 'id': node.key,
@ -24,7 +43,7 @@ class SerializeToTreeNodeMixin:
'title': _name(node), 'title': _name(node),
'pId': node.parent_key, 'pId': node.parent_key,
'isParent': True, 'isParent': True,
'open': True, 'open': _open(node),
'meta': { 'meta': {
'data': { 'data': {
"id": node.id, "id": node.id,
@ -52,7 +71,7 @@ class SerializeToTreeNodeMixin:
{ {
'id': str(asset.id), 'id': str(asset.id),
'name': asset.name, 'name': asset.name,
'title': asset.address, 'title': f'{asset.address}\n{asset.comment}',
'pId': get_pid(asset), 'pId': get_pid(asset),
'isParent': False, 'isParent': False,
'open': False, 'open': False,
@ -64,6 +83,8 @@ class SerializeToTreeNodeMixin:
'platform_type': asset.platform.type, 'platform_type': asset.platform.type,
'org_name': asset.org_name, 'org_name': asset.org_name,
'sftp': asset.platform_id in sftp_enabled_platform, 'sftp': asset.platform_id in sftp_enabled_platform,
'name': asset.name,
'address': asset.address
}, },
} }
} }

View File

@ -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.const import AllTypes
from assets.models import Platform from assets.models import Platform, Node, Asset
from assets.serializers import PlatformSerializer from assets.serializers import PlatformSerializer
from common.api import JMSModelViewSet from common.api import JMSModelViewSet
from common.permissions import IsValidUser
from common.serializers import GroupedChoiceSerializer from common.serializers import GroupedChoiceSerializer
__all__ = ['AssetPlatformViewSet'] __all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi']
class AssetPlatformViewSet(JMSModelViewSet): class AssetPlatformViewSet(JMSModelViewSet):
@ -18,12 +24,13 @@ class AssetPlatformViewSet(JMSModelViewSet):
rbac_perms = { rbac_perms = {
'categories': 'assets.view_platform', 'categories': 'assets.view_platform',
'type_constraints': '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): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.filter(type__in=AllTypes.get_types()) queryset = queryset.filter(type__in=AllTypes.get_types_values())
return queryset return queryset
def get_object(self): def get_object(self):
@ -38,3 +45,44 @@ class AssetPlatformViewSet(JMSModelViewSet):
request, message={"detail": "Internal platform"} request, message={"detail": "Internal platform"}
) )
return super().check_object_permissions(request, obj) 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)

View File

@ -41,6 +41,26 @@ class BasePlaybookManager:
self.method_hosts_mapper = defaultdict(list) self.method_hosts_mapper = defaultdict(list)
self.playbooks = [] self.playbooks = []
self.gateway_servers = dict() 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 @property
def platform_automation_methods(self): def platform_automation_methods(self):
@ -101,8 +121,9 @@ class BasePlaybookManager:
return host return host
def host_callback(self, host, automation=None, **kwargs): def host_callback(self, host, automation=None, **kwargs):
enabled_attr = '{}_enabled'.format(self.__class__.method_type()) method_type = self.__class__.method_type()
method_attr = '{}_method'.format(self.__class__.method_type()) enabled_attr = '{}_enabled'.format(method_type)
method_attr = '{}_method'.format(method_type)
method_enabled = automation and \ method_enabled = automation and \
getattr(automation, enabled_attr) and \ getattr(automation, enabled_attr) and \
@ -114,6 +135,7 @@ class BasePlaybookManager:
return host return host
host = self.convert_cert_to_file(host, kwargs.get('path_dir')) host = self.convert_cert_to_file(host, kwargs.get('path_dir'))
host['params'] = self.get_params(automation, method_type)
return host return host
@staticmethod @staticmethod
@ -239,10 +261,12 @@ class BasePlaybookManager:
jms_asset, jms_gateway = host['jms_asset'], host.get('gateway') jms_asset, jms_gateway = host['jms_asset'], host.get('gateway')
if not jms_gateway: if not jms_gateway:
continue continue
server = SSHTunnelForwarder( server = SSHTunnelForwarder(
(jms_gateway['address'], jms_gateway['port']), (jms_gateway['address'], jms_gateway['port']),
ssh_username=jms_gateway['username'], ssh_username=jms_gateway['username'],
ssh_password=jms_gateway['secret'], ssh_password=jms_gateway['secret'],
ssh_pkey=jms_gateway['private_key_path'],
remote_bind_address=(jms_asset['address'], jms_asset['port']) remote_bind_address=(jms_asset['address'], jms_asset['port'])
) )
try: try:
@ -252,8 +276,8 @@ class BasePlaybookManager:
print('\033[31m %s \033[0m\n' % err_msg) print('\033[31m %s \033[0m\n' % err_msg)
not_valid.append(k) not_valid.append(k)
else: else:
jms_asset['address'] = '127.0.0.1' host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
jms_asset['port'] = server.local_bind_port host['ansible_port'] = jms_asset['port'] = server.local_bind_port
servers.append(server) servers.append(server)
# 网域不可连接的,就不继续执行此资源的后续任务了 # 网域不可连接的,就不继续执行此资源的后续任务了

View File

@ -29,7 +29,7 @@ class GatherFactsManager(BasePlaybookManager):
asset = self.host_asset_mapper.get(host) asset = self.host_asset_mapper.get(host)
if asset and info: if asset and info:
info = self.format_asset_info(asset.type, info) info = self.format_asset_info(asset.type, info)
asset.info = info asset.gathered_info = info
asset.save(update_fields=['info']) asset.save(update_fields=['gathered_info'])
else: else:
logger.error("Not found info: {}".format(host)) logger.error("Not found info: {}".format(host))

View File

@ -1,8 +1,9 @@
import os
import yaml
import json import json
import os
from functools import partial from functools import partial
import yaml
def check_platform_method(manifest, manifest_path): def check_platform_method(manifest, manifest_path):
required_keys = ['category', 'method', 'name', 'id', 'type'] required_keys = ['category', 'method', 'name', 'id', 'type']
@ -21,6 +22,15 @@ def check_platform_methods(methods):
raise ValueError("Duplicate id: {}".format(_id)) 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): def get_platform_automation_methods(path):
methods = [] methods = []
for root, dirs, files in os.walk(path, topdown=False): 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) manifest = yaml.safe_load(f)
check_platform_method(manifest, path) check_platform_method(manifest, path)
manifest['dir'] = os.path.dirname(path) manifest['dir'] = os.path.dirname(path)
manifest['params_serializer'] = generate_serializer(manifest)
methods.append(manifest) methods.append(manifest)
check_platform_methods(methods) check_platform_methods(methods)
@ -46,12 +57,12 @@ def filter_key(manifest, attr, value):
return value in manifest_value or 'all' in manifest_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 methods = platform_automation_methods if methods is None else methods
if category: if category:
methods = filter(partial(filter_key, attr='category', value=category), methods) methods = filter(partial(filter_key, attr='category', value=category), methods)
if tp: if tp_name:
methods = filter(partial(filter_key, attr='type', value=tp), methods) methods = filter(partial(filter_key, attr='type', value=tp_name), methods)
if method: if method:
methods = filter(lambda x: x['method'] == method, methods) methods = filter(lambda x: x['method'] == method, methods)
return methods return methods

View File

@ -4,10 +4,21 @@ from jumpserver.utils import has_valid_xpack_license
from .protocol import Protocol 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): class BaseType(TextChoices):
""" """
约束应该考虑代是对平台对限制避免多余对选项: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain目前还不支持 约束应该考虑代是对平台对限制避免多余对选项: mysql 开启 ssh,
或者开启了也没有作用, 比如 k8s 开启了 domain目前还不支持
""" """
@classmethod @classmethod
def get_constrains(cls): def get_constrains(cls):
constrains = {} constrains = {}
@ -20,7 +31,7 @@ class BaseType(TextChoices):
protocols_default = protocols.pop('*', {}) protocols_default = protocols.pop('*', {})
automation_default = automation.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_base = {**base_default, **base.get(k, {})}
tp_auto = {**automation_default, **automation.get(k, {})} tp_auto = {**automation_default, **automation.get(k, {})}
tp_protocols = {**protocols_default, **protocols.get(k, {})} tp_protocols = {**protocols_default, **protocols.get(k, {})}
@ -35,8 +46,12 @@ class BaseType(TextChoices):
choices = protocol.get('choices', []) choices = protocol.get('choices', [])
if choices == '__self__': if choices == '__self__':
choices = [tp] choices = [tp]
protocols = [{'name': name, **settings.get(name, {})} for name in choices] protocols = [
protocols[0]['primary'] = True {'name': name, **settings.get(name, {})}
for name in choices
]
if protocols:
protocols[0]['default'] = True
return protocols return protocols
@classmethod @classmethod
@ -56,23 +71,21 @@ class BaseType(TextChoices):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def get_community_types(cls): def _get_choices_to_types(cls):
raise NotImplementedError choices = cls.get_choices()
return [Type(label, value) for value, label in choices]
@classmethod @classmethod
def get_types(cls): def get_types(cls):
tps = [tp for tp in cls] tps = cls._get_choices_to_types()
if not has_valid_xpack_license(): if not has_valid_xpack_license():
tps = cls.get_community_types() tps = cls.get_community_types()
return tps return tps
@classmethod
def get_community_types(cls):
return cls._get_choices_to_types()
@classmethod @classmethod
def get_choices(cls): def get_choices(cls):
tps = cls.get_types() return cls.choices
cls_choices = cls.choices
return [
choice for choice in cls_choices
if choice[0] in tps
]

View File

@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
from common.db.models import ChoicesMixin from common.db.models import ChoicesMixin
__all__ = ['Category'] __all__ = ['Category']
@ -13,13 +12,10 @@ class Category(ChoicesMixin, models.TextChoices):
DATABASE = 'database', _("Database") DATABASE = 'database', _("Database")
CLOUD = 'cloud', _("Cloud service") CLOUD = 'cloud', _("Cloud service")
WEB = 'web', _("Web") WEB = 'web', _("Web")
CUSTOM = 'custom', _("Custom type")
@classmethod @classmethod
def filter_choices(cls, category): def filter_choices(cls, category):
_category = getattr(cls, category.upper(), None) _category = getattr(cls, category.upper(), None)
choices = [(_category.value, _category.label)] if _category else cls.choices choices = [(_category.value, _category.label)] if _category else cls.choices
return choices return choices

View File

@ -15,6 +15,9 @@ class CloudTypes(BaseType):
'charset_enabled': False, 'charset_enabled': False,
'domain_enabled': False, 'domain_enabled': False,
'su_enabled': False, 'su_enabled': False,
},
cls.K8S: {
'domain_enabled': True,
} }
} }

View File

@ -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')

View File

@ -15,7 +15,8 @@ class DeviceTypes(BaseType):
'*': { '*': {
'charset_enabled': False, 'charset_enabled': False,
'domain_enabled': True, 'domain_enabled': True,
'su_enabled': False, 'su_enabled': True,
'su_methods': ['enable', 'super', 'super_level']
} }
} }

View File

@ -19,10 +19,7 @@ class HostTypes(BaseType):
'charset': 'utf-8', # default 'charset': 'utf-8', # default
'domain_enabled': True, 'domain_enabled': True,
'su_enabled': True, 'su_enabled': True,
'su_methods': [ 'su_methods': ['sudo', 'su'],
{'name': 'sudo su', 'id': 'sudo su'},
{'name': 'su -', 'id': 'su -'}
],
}, },
cls.WINDOWS: { cls.WINDOWS: {
'su_enabled': False, 'su_enabled': False,
@ -39,7 +36,7 @@ class HostTypes(BaseType):
'choices': ['ssh', 'telnet', 'vnc', 'rdp'] 'choices': ['ssh', 'telnet', 'vnc', 'rdp']
}, },
cls.WINDOWS: { cls.WINDOWS: {
'choices': ['rdp', 'ssh', 'vnc'] 'choices': ['rdp', 'ssh', 'vnc', 'winrm']
} }
} }
@ -61,7 +58,7 @@ class HostTypes(BaseType):
cls.WINDOWS: { cls.WINDOWS: {
'ansible_config': { 'ansible_config': {
'ansible_shell_type': 'cmd', 'ansible_shell_type': 'cmd',
'ansible_connection': 'ssh', 'ansible_connection': 'smart',
}, },
}, },
cls.OTHER_HOST: { cls.OTHER_HOST: {

View File

@ -10,6 +10,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
rdp = 'rdp', 'RDP' rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet' telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC' vnc = 'vnc', 'VNC'
winrm = 'winrm', 'WinRM'
mysql = 'mysql', 'MySQL' mysql = 'mysql', 'MySQL'
mariadb = 'mariadb', 'MariaDB' mariadb = 'mariadb', 'MariaDB'
@ -51,6 +52,13 @@ class Protocol(ChoicesMixin, models.TextChoices):
'port': 23, 'port': 23,
'secret_types': ['password'], 'secret_types': ['password'],
}, },
cls.winrm: {
'port': 5985,
'secret_types': ['password'],
'setting': {
'use_ssl': False,
}
},
} }
@classmethod @classmethod
@ -116,7 +124,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
'setting': { 'setting': {
'username_selector': 'name=username', 'username_selector': 'name=username',
'password_selector': 'name=password', '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.database_protocols(),
**cls.cloud_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()
}

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin from common.db.models import ChoicesMixin
from .category import Category from .category import Category
from .cloud import CloudTypes from .cloud import CloudTypes
from .custom import CustomTypes
from .database import DatabaseTypes from .database import DatabaseTypes
from .device import DeviceTypes from .device import DeviceTypes
from .host import HostTypes from .host import HostTypes
@ -16,7 +17,7 @@ class AllTypes(ChoicesMixin):
choices: list choices: list
includes = [ includes = [
HostTypes, DeviceTypes, DatabaseTypes, HostTypes, DeviceTypes, DatabaseTypes,
CloudTypes, WebTypes, CloudTypes, WebTypes, CustomTypes
] ]
_category_constrains = {} _category_constrains = {}
@ -24,22 +25,29 @@ class AllTypes(ChoicesMixin):
def choices(cls): def choices(cls):
choices = [] choices = []
for tp in cls.includes: for tp in cls.includes:
choices.extend(tp.choices) choices.extend(tp.get_choices())
return choices return choices
@classmethod
def get_choices(cls):
return cls.choices()
@classmethod @classmethod
def filter_choices(cls, category): 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 return choices() if callable(choices) else choices
@classmethod @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) types_cls = dict(cls.category_types()).get(category)
if not types_cls: if not types_cls:
return {} return {}
type_constraints = types_cls.get_constrains() type_constraints = types_cls.get_constrains()
constraints = type_constraints.get(tp, {}) constraints = type_constraints.get(tp_name, {})
cls.set_automation_methods(category, tp, constraints) cls.set_automation_methods(category, tp_name, constraints)
return constraints return constraints
@classmethod @classmethod
@ -56,7 +64,7 @@ class AllTypes(ChoicesMixin):
return asset_methods + account_methods return asset_methods + account_methods
@classmethod @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 from assets.automations import filter_platform_methods
automation = constraints.get('automation', {}) automation = constraints.get('automation', {})
automation_methods = {} automation_methods = {}
@ -66,7 +74,7 @@ class AllTypes(ChoicesMixin):
continue continue
item_name = item.replace('_enabled', '') item_name = item.replace('_enabled', '')
methods = filter_platform_methods( 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] methods = [{'name': m['name'], 'id': m['id']} for m in methods]
automation_methods[item_name + '_methods'] = methods automation_methods[item_name + '_methods'] = methods
@ -113,7 +121,7 @@ class AllTypes(ChoicesMixin):
@classmethod @classmethod
def grouped_choices(cls): 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 return grouped_types
@classmethod @classmethod
@ -136,16 +144,22 @@ class AllTypes(ChoicesMixin):
(Category.HOST, HostTypes), (Category.HOST, HostTypes),
(Category.DEVICE, DeviceTypes), (Category.DEVICE, DeviceTypes),
(Category.DATABASE, DatabaseTypes), (Category.DATABASE, DatabaseTypes),
(Category.CLOUD, CloudTypes),
(Category.WEB, WebTypes), (Category.WEB, WebTypes),
(Category.CLOUD, CloudTypes) (Category.CUSTOM, CustomTypes),
) )
@classmethod @classmethod
def get_types(cls): def get_types(cls):
tps = [] choices = []
for i in dict(cls.category_types()).values(): for i in dict(cls.category_types()).values():
tps.extend(i.get_types()) choices.extend(i.get_types())
return tps return choices
@classmethod
def get_types_values(cls):
choices = cls.get_types()
return [c.value for c in choices]
@staticmethod @staticmethod
def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None): def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None):

View File

@ -49,7 +49,10 @@ def migrate_asset_accounts(apps, schema_editor):
account_values.update(auth_book_auth) account_values.update(auth_book_auth)
auth_infos = [] auth_infos = []
username = account_values['username'] username = account_values.get('username')
if not username:
continue
for attr in auth_attrs: for attr in auth_attrs:
secret = account_values.pop(attr, None) secret = account_values.pop(attr, None)
if not secret: if not secret:

View File

@ -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),
]

View File

@ -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',
),
]

View File

@ -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),
]

View File

@ -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)
]

View File

@ -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'),
),
]

View File

@ -1,6 +1,7 @@
from .cloud import *
from .common import * from .common import *
from .host import * from .custom import *
from .database import * from .database import *
from .device import * from .device import *
from .host import *
from .web import * from .web import *
from .cloud import *

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import json import json
import logging import logging
from collections import defaultdict from collections import defaultdict
from django.db import models from django.db import models
from django.forms import model_to_dict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from assets import const from assets import const
@ -94,6 +94,20 @@ class Protocol(models.Model):
def __str__(self): def __str__(self):
return '{}/{}'.format(self.name, self.port) 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): class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
Category = const.Category Category = const.Category
@ -108,7 +122,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
verbose_name=_("Nodes")) verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) 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)() objects = AssetManager.from_queryset(AssetQuerySet)()
@ -148,20 +163,27 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return self.get_spec_values(instance, spec_fields) return self.get_spec_values(instance, spec_fields)
@lazyproperty @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 platform = self.platform
automation = self.platform.automation automation = self.platform.automation
return { auto_config = {
'su_enabled': platform.su_enabled, 'su_enabled': platform.su_enabled,
'ping_enabled': automation.ping_enabled,
'domain_enabled': platform.domain_enabled, 'domain_enabled': platform.domain_enabled,
'ansible_enabled': automation.ansible_enabled, 'ansible_enabled': False
'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,
} }
if not automation:
return auto_config
auto_config.update(model_to_dict(automation))
return auto_config
def get_target_ip(self): def get_target_ip(self):
return self.address return self.address
@ -191,25 +213,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
names.append(n.name + ':' + n.value) names.append(n.name + ':' + n.value)
return names 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 @lazyproperty
def type(self): def type(self):
return self.platform.type return self.platform.type
@ -275,6 +278,22 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
tree_node = TreeNode(**data) tree_node = TreeNode(**data)
return tree_node 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: class Meta:
unique_together = [('org_id', 'name')] unique_together = [('org_id', 'name')]
verbose_name = _("Asset") verbose_name = _("Asset")

View File

@ -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")

View File

@ -19,6 +19,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
type = models.CharField(max_length=16, verbose_name=_('Type')) type = models.CharField(max_length=16, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
params = models.JSONField(default=dict, verbose_name=_("Params"))
def __str__(self): def __str__(self):
return self.name + '@' + str(self.created_by) return self.name + '@' + str(self.created_by)

View File

@ -2,11 +2,10 @@
# #
from django.utils.translation import ugettext_lazy as _ 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.const import GATEWAY_NAME
from assets.models.platform import Platform
from common.utils import get_logger, lazyproperty from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgManager
from .asset.host import Host from .asset.host import Host
logger = get_logger(__file__) logger = get_logger(__file__)
@ -57,6 +56,14 @@ class Gateway(Host):
account = self.select_account account = self.select_account
return account.password if account else None 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 @lazyproperty
def private_key(self): def private_key(self):
account = self.select_account account = self.select_account

View File

@ -10,56 +10,68 @@ __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
class PlatformProtocol(models.Model): 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')) name = models.CharField(max_length=32, verbose_name=_('Name'))
port = models.IntegerField(verbose_name=_('Port')) 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) setting = models.JSONField(verbose_name=_('Setting'), default=dict)
platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols') platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols')
def __str__(self): def __str__(self):
return '{}/{}'.format(self.name, self.port) 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 @property
def secret_types(self): 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): class PlatformAutomation(models.Model):
ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled")) ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled"))
ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config")) ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config"))
ping_enabled = models.BooleanField(default=False, verbose_name=_("Ping enabled")) 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_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_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_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled"))
change_secret_method = models.TextField( change_secret_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Change secret method") 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_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
push_account_method = models.TextField( push_account_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Push account method") 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_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
verify_account_method = models.TextField( 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_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
gather_accounts_method = models.TextField( gather_accounts_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method") 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): class Platform(JMSBaseModel):
@ -80,14 +92,18 @@ class Platform(JMSBaseModel):
internal = models.BooleanField(default=False, verbose_name=_("Internal")) internal = models.BooleanField(default=False, verbose_name=_("Internal"))
# 资产有关的 # 资产有关的
charset = models.CharField( 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")) domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
# 账号有关的 # 账号有关的
su_enabled = models.BooleanField(default=False, verbose_name=_("Su 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")) 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', automation = models.OneToOneField(
blank=True, null=True, verbose_name=_("Automation")) 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 @property
def type_constraints(self): def type_constraints(self):
@ -100,11 +116,6 @@ class Platform(JMSBaseModel):
) )
return linux.id 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): def __str__(self):
return self.name return self.name

View File

@ -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'<ol>{"".join([f"<li>{asset}</li>" for asset in self.assets])}</ol>'
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)

View File

@ -1,6 +1,8 @@
# No pass
from .cloud import *
from .common import * from .common import *
from .host import * from .custom import *
from .database import * from .database import *
from .device import * from .device import *
from .cloud import * from .host import *
from .web import * from .web import *

View File

@ -1,15 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re
from django.db.models import F from django.db.models import F
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.models import Account from accounts.models import Account
from accounts.serializers import AccountSerializerCreateValidateMixin from accounts.serializers import AccountSerializer
from accounts.serializers import AuthValidateMixin from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer MethodSerializer
from common.serializers.dynamic import create_serializer_class
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes from ...const import Category, AllTypes
@ -19,9 +22,11 @@ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer', '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): class AssetProtocolsSerializer(serializers.ModelSerializer):
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1) 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'] fields = ['name', 'port']
class AssetProtocolsPermsSerializer(AssetProtocolsSerializer):
class Meta(AssetProtocolsSerializer.Meta):
fields = AssetProtocolsSerializer.Meta.fields + ['public', 'setting']
class AssetLabelSerializer(serializers.ModelSerializer): class AssetLabelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Label model = Label
@ -59,45 +69,39 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
} }
class AssetAccountSerializer( class AssetAccountSerializer(AccountSerializer):
AuthValidateMixin,
AccountSerializerCreateValidateMixin,
CommonModelSerializer
):
add_org_fields = False add_org_fields = False
push_now = serializers.BooleanField( asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True)
default=False, label=_("Push now"), write_only=True clone_id: str
)
template = serializers.BooleanField(
default=False, label=_("Template"), write_only=True
)
name = serializers.CharField(max_length=128, required=True, label=_("Name"))
class Meta: def to_internal_value(self, data):
model = Account clone_id = data.pop('id', None)
fields_mini = [ ret = super().to_internal_value(data)
'id', 'name', 'username', 'privileged', self.clone_id = clone_id
'is_active', 'version', 'secret_type', 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 = { 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 AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
class Meta: 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): class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account')) 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: class Meta:
model = Asset model = Asset
fields_mini = ['id', 'name', 'address'] 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_fk = ['domain', 'platform']
fields_m2m = [ fields_m2m = [
'nodes', 'labels', 'protocols', 'nodes', 'labels', 'protocols',
'nodes_display', 'accounts' 'nodes_display', 'accounts',
] ]
read_only_fields = [ read_only_fields = [
'category', 'type', 'connectivity', 'auto_info', 'category', 'type', 'connectivity', 'auto_config',
'date_verified', 'created_by', 'date_created', 'date_verified', 'created_by', 'date_created',
] ]
fields = fields_small + fields_fk + fields_m2m + read_only_fields fields = fields_small + fields_fk + fields_m2m + read_only_fields
fields_unexport = ['auto_info'] fields_unexport = ['auto_config']
extra_kwargs = { extra_kwargs = {
'auto_info': {'label': _('Auto info')}, 'auto_config': {'label': _('Auto info')},
'name': {'label': _("Name")}, 'name': {'label': _("Name")},
'address': {'label': _('Address')}, 'address': {'label': _('Address')},
'nodes_display': {'label': _('Node path')}, 'nodes_display': {'label': _('Node path')},
@ -197,6 +189,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
.annotate(type=F("platform__type")) .annotate(type=F("platform__type"))
return queryset 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 @staticmethod
def perform_nodes_display_create(instance, nodes_display): def perform_nodes_display_create(instance, nodes_display):
if not nodes_display: if not nodes_display:
@ -276,8 +298,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
if not accounts_data: if not accounts_data:
return return
for data in accounts_data: for data in accounts_data:
data['asset'] = asset data['asset'] = asset.id
AssetAccountSerializer().create(data)
s = AssetAccountSerializer(data=accounts_data, many=True)
s.is_valid(raise_exception=True)
s.save()
@atomic @atomic
def create(self, validated_data): def create(self, validated_data):
@ -300,16 +325,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
class DetailMixin(serializers.Serializer): class DetailMixin(serializers.Serializer):
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
spec_info = serializers.DictField(label=_('Spec info'), read_only=True) spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
auto_info = serializers.DictField(read_only=True, label=_('Auto info')) 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): def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info) names = super().get_field_names(declared_fields, info)
names.extend([ names.extend([
'accounts', 'info', 'spec_info', 'auto_info' 'accounts', 'gathered_info', 'spec_info',
'auto_config',
]) ])
return names 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): class AssetDetailSerializer(DetailMixin, AssetSerializer):
pass pass

View File

@ -0,0 +1,9 @@
from assets.models import Custom
from .common import AssetSerializer
__all__ = ['CustomSerializer']
class CustomSerializer(AssetSerializer):
class Meta(AssetSerializer.Meta):
model = Custom

View File

@ -1,9 +1,9 @@
from rest_framework.serializers import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.serializers import ValidationError
from assets.models import Database from assets.models import Database
from assets.serializers.gateway import GatewayWithAccountSecretSerializer
from .common import AssetSerializer from .common import AssetSerializer
from ..gateway import GatewayWithAccountSecretSerializer
__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer'] __all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer']

View File

@ -1,34 +1,18 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from assets.models import Host from assets.models import Host
from .common import AssetSerializer from .common import AssetSerializer
from .info.gathered import HostGatheredInfoSerializer
__all__ = ['HostInfoSerializer', 'HostSerializer'] __all__ = ['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'))
class HostSerializer(AssetSerializer): class HostSerializer(AssetSerializer):
info = HostInfoSerializer(required=False, label=_('Info')) gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info"))
class Meta(AssetSerializer.Meta): class Meta(AssetSerializer.Meta):
model = Host model = Host
fields = AssetSerializer.Meta.fields + ['info'] fields = AssetSerializer.Meta.fields + ['gathered_info']
extra_kwargs = { extra_kwargs = {
**AssetSerializer.Meta.extra_kwargs, **AssetSerializer.Meta.extra_kwargs,
'address': { 'address': {

View File

@ -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,
}

View File

@ -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,
}

View File

@ -24,6 +24,6 @@ class WebSerializer(AssetSerializer):
'default': 'name=password' 'default': 'name=password'
}, },
'submit_selector': { 'submit_selector': {
'default': 'id=longin_button', 'default': 'id=login_button',
}, },
} }

View File

@ -3,8 +3,8 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from .asset import HostSerializer
from .asset.common import AccountSecretSerializer from .asset.common import AccountSecretSerializer
from .asset.host import HostSerializer
from ..models import Gateway, Asset from ..models import Gateway, Asset
__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer'] __all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer']

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from assets.const.web import FillType from assets.const.web import FillType
from common.serializers import WritableNestedModelSerializer from common.serializers import WritableNestedModelSerializer
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty
from ..const import Category, AllTypes from ..const import Category, AllTypes
from ..models import Platform, PlatformProtocol, PlatformAutomation from ..models import Platform, PlatformProtocol, PlatformAutomation
@ -37,10 +38,12 @@ class ProtocolSettingSerializer(serializers.Serializer):
default="", allow_blank=True, label=_("Submit selector") default="", allow_blank=True, label=_("Submit selector")
) )
script = serializers.JSONField(default=list, label=_("Script")) script = serializers.JSONField(default=list, label=_("Script"))
# Redis # Redis
auth_username = serializers.BooleanField(default=False, label=_("Auth with username")) 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 PlatformAutomationSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -48,12 +51,12 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
fields = [ fields = [
"id", "id",
"ansible_enabled", "ansible_config", "ansible_enabled", "ansible_config",
"ping_enabled", "ping_method", "ping_enabled", "ping_method", "ping_params",
"push_account_enabled", "push_account_method", "push_account_enabled", "push_account_method", "push_account_params",
"gather_facts_enabled", "gather_facts_method", "gather_facts_enabled", "gather_facts_method", "gather_facts_params",
"change_secret_enabled", "change_secret_method", "change_secret_enabled", "change_secret_method", "change_secret_params",
"verify_account_enabled", "verify_account_method", "verify_account_enabled", "verify_account_method", "verify_account_params",
"gather_accounts_enabled", "gather_accounts_method", "gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
] ]
extra_kwargs = { 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) setting = ProtocolSettingSerializer(required=False, allow_null=True)
primary = serializers.BooleanField(read_only=True, label=_("Primary"))
class Meta: class Meta:
model = PlatformProtocol model = PlatformProtocol
fields = [ fields = [
"id", "name", "port", "primary", "id", "name", "port", "primary",
"default", "required", "secret_types", "required", "default",
"setting", "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): class PlatformSerializer(WritableNestedModelSerializer):
charset = LabeledChoiceField( SU_METHOD_CHOICES = [
choices=Platform.CharsetChoices.choices, label=_("Charset") ("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")) type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
category = LabeledChoiceField(choices=Category.choices, label=_("Category")) category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
protocols = PlatformProtocolsSerializer( protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
label=_("Protocols"), many=True, required=False automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
su_method = LabeledChoiceField( su_method = LabeledChoiceField(
choices=[("sudo", "sudo su -"), ("su", "su - ")], choices=SU_METHOD_CHOICES, label=_("Su method"),
label=_("Su method"), required=False, default="sudo", allow_null=True required=False, default="sudo", allow_null=True
) )
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
class Meta: class Meta:
model = Platform model = Platform
@ -106,19 +129,54 @@ class PlatformSerializer(WritableNestedModelSerializer):
fields_small = fields_mini + [ fields_small = fields_mini + [
"category", "type", "charset", "category", "type", "charset",
] ]
fields_other = [ read_only_fields = [
'date_created', 'date_updated', 'created_by', 'updated_by', 'internal', 'date_created', 'date_updated',
'created_by', 'updated_by'
] ]
fields = fields_small + [ fields = fields_small + [
"protocols", "domain_enabled", "su_enabled", "protocols", "domain_enabled", "su_enabled",
"su_method", "automation", "comment", "su_method", "automation", "comment", "custom_fields",
] + fields_other ] + read_only_fields
extra_kwargs = { extra_kwargs = {
"su_enabled": {"label": _('Su enabled')}, "su_enabled": {"label": _('Su enabled')},
"domain_enabled": {"label": _('Domain enabled')}, "domain_enabled": {"label": _('Domain enabled')},
"domain_default": {"label": _('Default Domain')}, "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 @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(
@ -126,6 +184,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
) )
return queryset 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): class PlatformOpsMethodSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True) id = serializers.CharField(read_only=True)

View File

@ -66,11 +66,11 @@ def on_asset_create(sender, instance=None, created=False, **kwargs):
ensure_asset_has_node(assets=(instance,)) ensure_asset_has_node(assets=(instance,))
# 获取资产硬件信息 # 获取资产硬件信息
auto_info = instance.auto_info auto_config = instance.auto_config
if auto_info.get('ping_enabled'): if auto_config.get('ping_enabled'):
logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name)) logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name))
test_assets_connectivity_handler(assets=(instance,)) 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)) logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name))
gather_assets_facts_handler(assets=(instance,)) gather_assets_facts_handler(assets=(instance,))

View File

@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device')
router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'webs', api.WebViewSet, 'web') router.register(r'webs', api.WebViewSet, 'web')
router.register(r'clouds', api.CloudViewSet, 'cloud') router.register(r'clouds', api.CloudViewSet, 'cloud')
router.register(r'customs', api.CustomViewSet, 'custom')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'labels', api.LabelViewSet, 'label') router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'nodes', api.NodeViewSet, 'node')
@ -45,6 +46,7 @@ urlpatterns = [
path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), path('gateways/<uuid:pk>/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 urlpatterns += router.urls

View File

@ -162,7 +162,8 @@ class RDPFileClientProtocolURLMixin:
def get_smart_endpoint(self, protocol, asset=None): def get_smart_endpoint(self, protocol, asset=None):
target_ip = asset.get_target_ip() if asset else '' target_ip = asset.get_target_ip() if asset else ''
endpoint = EndpointRule.match_endpoint( 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 return endpoint

View File

@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from assets.const import Protocol from assets.const import Protocol
from assets.const.host import GATEWAY_NAME
from common.db.fields import EncryptTextField from common.db.fields import EncryptTextField
from common.exceptions import JMSException from common.exceptions import JMSException
from common.utils import lazyproperty, pretty_string, bulk_get from common.utils import lazyproperty, pretty_string, bulk_get
@ -231,12 +232,14 @@ class ConnectionToken(JMSOrgBaseModel):
def domain(self): def domain(self):
if not self.asset.platform.domain_enabled: if not self.asset.platform.domain_enabled:
return 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 return domain
@lazyproperty @lazyproperty
def gateway(self): def gateway(self):
if not self.asset: if not self.asset or not self.domain:
return return
return self.asset.gateway return self.asset.gateway

View File

@ -5,7 +5,8 @@ from accounts.const import SecretType
from accounts.models import Account from accounts.models import Account
from acls.models import CommandGroup, CommandFilterACL from acls.models import CommandGroup, CommandFilterACL
from assets.models import Asset, Platform, Gateway, Domain 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 LabeledChoiceField
from common.serializers.fields import ObjectRelatedField from common.serializers.fields import ObjectRelatedField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
@ -26,18 +27,17 @@ class _ConnectionTokenUserSerializer(serializers.ModelSerializer):
class _ConnectionTokenAssetSerializer(serializers.ModelSerializer): class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
info = serializers.DictField()
class Meta: class Meta:
model = Asset model = Asset
fields = [ fields = [
'id', 'name', 'address', 'protocols', 'id', 'name', 'address', 'protocols', 'category',
'category', 'type', 'org_id', 'spec_info', 'type', 'org_id', 'info', 'secret_info', 'spec_info'
'secret_info',
] ]
class _SimpleAccountSerializer(serializers.ModelSerializer): class _SimpleAccountSerializer(serializers.ModelSerializer):
""" Account """
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta: class Meta:
@ -46,20 +46,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
""" Account """
su_from = _SimpleAccountSerializer(required=False, label=_('Su from')) su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged' 'id', 'name', 'username', 'secret_type',
'secret', 'su_from', 'privileged'
] ]
class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer): class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
""" Gateway """
account = _SimpleAccountSerializer( account = _SimpleAccountSerializer(
required=False, source='select_account', read_only=True required=False, source='select_account', read_only=True
) )
@ -85,7 +83,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CommandFilterACL model = CommandFilterACL
fields = [ 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', 'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'protocol', 'platform', 'command_filter_acls', 'protocol',
'domain', 'gateway', 'actions', 'expire_at', 'domain', 'gateway', 'actions', 'expire_at',
'from_ticket', 'from_ticket', 'expire_now', 'connect_method',
'expire_now', 'connect_method',
] ]
extra_kwargs = { extra_kwargs = {
'value': {'read_only': True}, 'value': {'read_only': True},

View File

@ -111,7 +111,7 @@ class BaseFileParser(BaseParser):
return {'pk': obj_id, 'name': obj_name} return {'pk': obj_id, 'name': obj_name}
def parse_value(self, field, value): def parse_value(self, field, value):
if value is '-': if value == '-' and field and field.allow_null:
return None return None
elif hasattr(field, 'to_file_internal_value'): elif hasattr(field, 'to_file_internal_value'):
value = field.to_file_internal_value(value) value = field.to_file_internal_value(value)

View File

@ -1,2 +1,3 @@
from .common import * from .common import *
from .dynamic import *
from .mixin import * from .mixin import *

View File

@ -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)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import phonenumbers
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -17,6 +18,7 @@ __all__ = [
"BitChoicesField", "BitChoicesField",
"TreeChoicesField", "TreeChoicesField",
"LabeledMultipleChoiceField", "LabeledMultipleChoiceField",
"PhoneField",
] ]
@ -201,3 +203,11 @@ class BitChoicesField(TreeChoicesField):
value = self.to_internal_value(data) value = self.to_internal_value(data)
self.run_validators(value) self.run_validators(value)
return 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

View File

@ -2,12 +2,15 @@
# #
import re import re
import phonenumbers
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.validators import ( from rest_framework.validators import (
UniqueTogetherValidator, ValidationError UniqueTogetherValidator, ValidationError
) )
from rest_framework import serializers from rest_framework import serializers
from phonenumbers.phonenumberutil import NumberParseException
from common.utils.strings import no_special_chars from common.utils.strings import no_special_chars
@ -42,9 +45,14 @@ class NoSpecialChars:
class PhoneValidator: class PhoneValidator:
pattern = re.compile(r"^1[3456789]\d{9}$")
message = _('The mobile phone number format is incorrect') message = _('The mobile phone number format is incorrect')
def __call__(self, value): 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) raise serializers.ValidationError(self.message)

View File

@ -1,15 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils import translation 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.contrib.auth.mixins import UserPassesTestMixin
from django.http.response import JsonResponse from django.http.response import JsonResponse
from rest_framework import permissions from rest_framework import permissions
from rest_framework.request import Request from rest_framework.request import Request
from common.exceptions import UserConfirmRequired from common.exceptions import UserConfirmRequired
from common.utils import i18n_fmt
from audits.handler import create_or_update_operate_log 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__ = [ __all__ = [
"PermissionsMixin", "PermissionsMixin",
@ -49,38 +51,63 @@ class RecordViewLogMixin:
ACTION = ActionChoices.view ACTION = ActionChoices.view
@staticmethod @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) query_params = dict(request.query_params)
if query_params.get("format"): params = self._filter_params(query_params)
query_params.pop("format")
spm_filter = query_params.pop("spm") if query_params.get("spm") else None spm_filter = params.pop("spm", None)
if not query_params and not spm_filter:
display_message = _("Export all") if not params and not spm_filter:
display_message = gettext_noop("Export all")
elif spm_filter: elif spm_filter:
display_message = _("Export only selected items") display_message = gettext_noop("Export only selected items")
else: else:
query = ",".join( 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 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): def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
with translation.override('en'): with translation.override('en'):
resource_display = self.get_resource_display(request) resource_display = self.get_resource_display(request)
resource_type = self.model._meta.verbose_name ids = [q.id for q in self.get_queryset()]
create_or_update_operate_log( self.record_logs(ids, resource_display=resource_display)
self.ACTION, resource_type, force=True,
resource_display=resource_display
)
return response return response
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs) response = super().retrieve(request, *args, **kwargs)
with translation.override('en'): with translation.override('en'):
resource_type = self.model._meta.verbose_name resource = self.get_object()
create_or_update_operate_log( self.record_logs([resource.id], resource=resource)
self.ACTION, resource_type, force=True, resource=self.get_object()
)
return response return response

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6fa80b59b9b5f95a9cfcad8ec47eacd519bb962d139ab90463795a7b306a0a72 oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
size 137935 size 138303

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More