mirror of https://github.com/jumpserver/jumpserver
commit
68351b1c39
|
@ -8,4 +8,4 @@ celerybeat.pid
|
|||
### Vagrant ###
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
|
||||
.history/
|
||||
|
|
|
@ -3,7 +3,10 @@ name: 需求建议
|
|||
about: 提出针对本项目的想法和建议
|
||||
title: "[Feature] "
|
||||
labels: 类型:需求
|
||||
assignees: ibuler
|
||||
assignees:
|
||||
- ibuler
|
||||
- baijiangjie
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ name: Bug 提交
|
|||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: 类型:bug
|
||||
assignees: wojiushixiaobai
|
||||
assignees:
|
||||
- wojiushixiaobai
|
||||
- baijiangjie
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ name: 问题咨询
|
|||
about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 类型:提问
|
||||
assignees: wojiushixiaobai
|
||||
assignees:
|
||||
- wojiushixiaobai
|
||||
- baijiangjie
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -43,3 +43,4 @@ releashe
|
|||
/apps/script.py
|
||||
data/*
|
||||
test.py
|
||||
.history/
|
||||
|
|
|
@ -55,7 +55,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
|||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
|
|
|
@ -53,7 +53,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
|||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
|
|
|
@ -54,6 +54,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运
|
|||
|
||||
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
|
||||
- [产品文档](https://docs.jumpserver.org)
|
||||
- [在线学习](https://edu.fit2cloud.com/page/2635362)
|
||||
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
|
||||
|
||||
## 案例研究
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.filters import AccountFilterSet
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
__all__ = [
|
||||
'AccountViewSet', 'AccountSecretsViewSet',
|
||||
'AccountHistoriesSecretAPI'
|
||||
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
||||
]
|
||||
|
||||
|
||||
|
@ -28,7 +29,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||
rbac_perms = {
|
||||
'partial_update': ['accounts.change_account'],
|
||||
'su_from_accounts': 'accounts.view_account',
|
||||
'username_suggestions': 'accounts.view_account',
|
||||
'clear_secret': 'accounts.change_account',
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-accounts')
|
||||
|
@ -48,7 +49,10 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||
serializer = serializers.AccountSerializer(accounts, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='username-suggestions')
|
||||
@action(
|
||||
methods=['get'], detail=False, url_path='username-suggestions',
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
asset_ids = request.query_params.get('assets')
|
||||
node_keys = request.query_params.get('keys')
|
||||
|
@ -71,6 +75,12 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||
usernames = common + others
|
||||
return Response(data=usernames)
|
||||
|
||||
@action(methods=['patch'], detail=False, url_path='clear-secret')
|
||||
def clear_secret(self, request, *args, **kwargs):
|
||||
account_ids = request.data.get('account_ids', [])
|
||||
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
||||
return Response(status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
|
@ -87,6 +97,20 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
|||
}
|
||||
|
||||
|
||||
class AssetAccountBulkCreateApi(CreateAPIView):
|
||||
serializer_class = serializers.AssetAccountBulkSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'accounts.add_account',
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.create(serializer.validated_data)
|
||||
serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True)
|
||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
|
||||
model = Account.history.model
|
||||
serializer_class = serializers.AccountHistorySerializer
|
||||
|
|
|
@ -31,8 +31,8 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
|||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import Source
|
||||
from accounts.filters import GatheredAccountFilterSet
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
from accounts.models import GatheredAccount
|
||||
|
@ -50,22 +48,12 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
|
|||
'default': serializers.GatheredAccountSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'sync_account': 'assets.add_gatheredaccount',
|
||||
'sync_accounts': 'assets.add_gatheredaccount',
|
||||
}
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='sync')
|
||||
def sync_account(self, request, *args, **kwargs):
|
||||
gathered_account = super().get_object()
|
||||
asset = gathered_account.asset
|
||||
username = gathered_account.username
|
||||
accounts = asset.accounts.filter(username=username)
|
||||
|
||||
if accounts.exists():
|
||||
accounts.update(source=Source.COLLECTED)
|
||||
else:
|
||||
asset.accounts.model.objects.create(
|
||||
asset=asset, username=username,
|
||||
name=f'{username}-{_("Collected")}',
|
||||
source=Source.COLLECTED
|
||||
)
|
||||
@action(methods=['post'], detail=False, url_path='sync-accounts')
|
||||
def sync_accounts(self, request, *args, **kwargs):
|
||||
gathered_account_ids = request.data.get('gathered_account_ids')
|
||||
gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids)
|
||||
self.model.sync_accounts(gathered_accounts)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
|
|
@ -18,18 +18,18 @@
|
|||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
|
|
|
@ -18,18 +18,18 @@
|
|||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
|
|
|
@ -42,7 +42,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
def method_type(cls):
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
def get_kwargs(self, account, secret, secret_type):
|
||||
def get_ssh_params(self, account, secret, secret_type):
|
||||
kwargs = {}
|
||||
if secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
|
@ -76,6 +76,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
accounts = accounts.filter(id__in=self.account_ids)
|
||||
if self.secret_type:
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
|
||||
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
||||
accounts = accounts.filter(privileged=False).exclude(
|
||||
username__in=['root', 'administrator']
|
||||
)
|
||||
return accounts
|
||||
|
||||
def host_callback(
|
||||
|
@ -106,6 +111,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
print(f'Windows {asset} does not support ssh key push')
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
|
@ -124,7 +130,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret, secret_type)
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
|
|
|
@ -12,6 +12,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_asset_mapper = {}
|
||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
|
@ -25,26 +26,38 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||
def filter_success_result(self, tp, result):
|
||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def update_or_create_gathered_accounts(asset, result):
|
||||
def generate_data(asset, result):
|
||||
data = []
|
||||
for username, info in result.items():
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if info.get('date'):
|
||||
d['date_last_login'] = info['date']
|
||||
if info.get('address'):
|
||||
d['address_last_login'] = info['address'][:32]
|
||||
data.append(d)
|
||||
return data
|
||||
|
||||
def update_or_create_accounts(self, asset, result):
|
||||
data = self.generate_data(asset, result)
|
||||
with tmp_to_org(asset.org_id):
|
||||
gathered_accounts = []
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for username, data in result.items():
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if data.get('date'):
|
||||
d['date_last_login'] = data['date']
|
||||
if data.get('address'):
|
||||
d['address_last_login'] = data['address'][:32]
|
||||
GatheredAccount.objects.update_or_create(
|
||||
for d in data:
|
||||
username = d['username']
|
||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
||||
defaults=d, asset=asset, username=username,
|
||||
)
|
||||
gathered_accounts.append(gathered_account)
|
||||
if not self.is_sync_account:
|
||||
return
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
info = result.get('debug', {}).get('res', {}).get('info', {})
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
result = self.filter_success_result(asset.type, info)
|
||||
self.update_or_create_gathered_accounts(asset, result)
|
||||
self.update_or_create_accounts(asset, result)
|
||||
else:
|
||||
logger.error("Not found info".format(host))
|
||||
|
|
|
@ -1,30 +1,6 @@
|
|||
import os
|
||||
import copy
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from assets.automations.methods import get_platform_automation_methods
|
||||
|
||||
|
||||
def copy_change_secret_to_push_account(methods):
|
||||
push_account = AutomationTypes.push_account
|
||||
change_secret = AutomationTypes.change_secret
|
||||
copy_methods = copy.deepcopy(methods)
|
||||
for method in copy_methods:
|
||||
if not method['id'].startswith(change_secret):
|
||||
continue
|
||||
copy_method = copy.deepcopy(method)
|
||||
copy_method['method'] = push_account.value
|
||||
copy_method['id'] = copy_method['id'].replace(
|
||||
change_secret, push_account
|
||||
)
|
||||
copy_method['name'] = copy_method['name'].replace(
|
||||
'Change secret', 'Push account'
|
||||
)
|
||||
methods.append(copy_method)
|
||||
return methods
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
automation_methods = get_platform_automation_methods(BASE_DIR)
|
||||
|
||||
platform_automation_methods = copy_change_secret_to_push_account(automation_methods)
|
||||
platform_automation_methods = get_platform_automation_methods(BASE_DIR)
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
id: push_account_mongodb
|
||||
name: Push account for MongoDB
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: push_account
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
id: push_account_mysql
|
||||
name: Push account for MySQL
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: push_account
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
id: push_account_oracle
|
||||
name: Push account for Oracle
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: push_account
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
id: push_account_postgresql
|
||||
name: Push account for PostgreSQL
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: push_account
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
id: push_account_sqlserver
|
||||
name: Push account for SQLServer
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: push_account
|
|
@ -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"
|
|
@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
|
|
@ -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"
|
|
@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
|
@ -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"
|
|
@ -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: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
|
@ -31,6 +31,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
|||
print(msg)
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
|
@ -49,7 +50,7 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
|||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret, secret_type)
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
|
|
|
@ -18,3 +18,10 @@ class AliasAccount(TextChoices):
|
|||
class Source(TextChoices):
|
||||
LOCAL = 'local', _('Local')
|
||||
COLLECTED = 'collected', _('Collected')
|
||||
TEMPLATE = 'template', _('Template')
|
||||
|
||||
|
||||
class AccountInvalidPolicy(TextChoices):
|
||||
SKIP = 'skip', _('Skip')
|
||||
UPDATE = 'update', _('Update')
|
||||
ERROR = 'error', _('Failed')
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -53,6 +53,7 @@ class Account(AbsConnectivity, BaseAccount):
|
|||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account')
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import AutomationTypes, Source
|
||||
from accounts.models import Account
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from .base import AccountBaseAutomation
|
||||
|
||||
|
@ -19,6 +21,25 @@ class GatheredAccount(JMSOrgBaseModel):
|
|||
def address(self):
|
||||
return self.asset.address
|
||||
|
||||
@staticmethod
|
||||
def sync_accounts(gathered_accounts):
|
||||
account_objs = []
|
||||
for gathered_account in gathered_accounts:
|
||||
asset_id = gathered_account.asset_id
|
||||
username = gathered_account.username
|
||||
accounts = Account.objects.filter(
|
||||
Q(asset_id=asset_id, username=username) |
|
||||
Q(asset_id=asset_id, name=username)
|
||||
)
|
||||
if accounts.exists():
|
||||
continue
|
||||
account = Account(
|
||||
asset_id=asset_id, username=username,
|
||||
name=username, source=Source.COLLECTED
|
||||
)
|
||||
account_objs.append(account)
|
||||
Account.objects.bulk_create(account_objs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Gather account automation')
|
||||
unique_together = [
|
||||
|
@ -31,6 +52,17 @@ class GatheredAccount(JMSOrgBaseModel):
|
|||
|
||||
|
||||
class GatherAccountsAutomation(AccountBaseAutomation):
|
||||
is_sync_account = models.BooleanField(
|
||||
default=False, blank=True, verbose_name=_("Is sync account")
|
||||
)
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'is_sync_account': self.is_sync_account,
|
||||
})
|
||||
return attr_json
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = AutomationTypes.gather_accounts
|
||||
super().save(*args, **kwargs)
|
||||
|
|
|
@ -51,7 +51,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
|||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'username': self.username
|
||||
'username': self.username,
|
||||
'params': self.params,
|
||||
})
|
||||
return attr_json
|
||||
|
||||
|
|
|
@ -1,75 +1,172 @@
|
|||
import uuid
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from accounts.const import SecretType, Source
|
||||
from accounts.const import SecretType, Source, AccountInvalidPolicy
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin, BulkModelSerializer
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from common.utils import get_logger
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class AccountSerializerCreateValidateMixin:
|
||||
from_id: str
|
||||
template: bool
|
||||
push_now: bool
|
||||
replace_attrs: callable
|
||||
|
||||
def to_internal_value(self, data):
|
||||
from_id = data.pop('id', None)
|
||||
ret = super().to_internal_value(data)
|
||||
self.from_id = from_id
|
||||
return ret
|
||||
class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=AccountTemplate.objects,
|
||||
required=False, label=_("Template"), write_only=True
|
||||
)
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
params = serializers.JSONField(
|
||||
decoder=None, encoder=None, required=False, style={'base_template': 'textarea.html'}
|
||||
)
|
||||
on_invalid = LabeledChoiceField(
|
||||
choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
|
||||
write_only=True, label=_('Exist policy')
|
||||
)
|
||||
|
||||
def set_secret(self, attrs):
|
||||
_id = self.from_id
|
||||
template = attrs.pop('template', None)
|
||||
class Meta:
|
||||
fields = ['template', 'push_now', 'params', 'on_invalid']
|
||||
|
||||
if _id and template:
|
||||
account_template = AccountTemplate.objects.get(id=_id)
|
||||
attrs['secret'] = account_template.secret
|
||||
elif _id and not template:
|
||||
account = Account.objects.get(id=_id)
|
||||
attrs['secret'] = account.secret
|
||||
return attrs
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_initial_value()
|
||||
|
||||
def set_initial_value(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
return
|
||||
if isinstance(self.initial_data, dict):
|
||||
initial_data = [self.initial_data]
|
||||
else:
|
||||
initial_data = self.initial_data
|
||||
|
||||
for data in initial_data:
|
||||
if not data.get('asset') and not self.instance:
|
||||
raise serializers.ValidationError({'asset': 'Asset is required'})
|
||||
asset = data.get('asset') or self.instance.asset
|
||||
self.from_template_if_need(data)
|
||||
self.set_uniq_name_if_need(data, asset)
|
||||
|
||||
def set_uniq_name_if_need(self, initial_data, asset):
|
||||
name = initial_data.get('name')
|
||||
if name is None:
|
||||
return
|
||||
if not name:
|
||||
name = initial_data.get('username')
|
||||
if self.instance and self.instance.name == name:
|
||||
return
|
||||
if Account.objects.filter(name=name, asset=asset).exists():
|
||||
name = name + '_' + uuid.uuid4().hex[:4]
|
||||
initial_data['name'] = name
|
||||
|
||||
@staticmethod
|
||||
def from_template_if_need(initial_data):
|
||||
template_id = initial_data.get('template')
|
||||
if not template_id:
|
||||
return
|
||||
if isinstance(template_id, (str, uuid.UUID)):
|
||||
template = AccountTemplate.objects.filter(id=template_id).first()
|
||||
else:
|
||||
template = template_id
|
||||
if not template:
|
||||
raise serializers.ValidationError({'template': 'Template not found'})
|
||||
|
||||
# Set initial data from template
|
||||
ignore_fields = ['id', 'date_created', 'date_updated', 'org_id']
|
||||
field_names = [
|
||||
field.name for field in template._meta.fields
|
||||
if field.name not in ignore_fields
|
||||
]
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
attrs[name] = value
|
||||
initial_data.update(attrs)
|
||||
|
||||
@staticmethod
|
||||
def push_account_if_need(instance, push_now, params, stat):
|
||||
if not push_now or stat != 'created':
|
||||
return
|
||||
push_accounts_to_assets_task.delay([str(instance.id)], params)
|
||||
|
||||
def get_validators(self):
|
||||
_validators = super().get_validators()
|
||||
if getattr(self, 'initial_data', None) is None:
|
||||
return _validators
|
||||
on_invalid = self.initial_data.get('on_invalid')
|
||||
if on_invalid == AccountInvalidPolicy.ERROR:
|
||||
return _validators
|
||||
_validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)]
|
||||
return _validators
|
||||
|
||||
@staticmethod
|
||||
def do_create(vd):
|
||||
on_invalid = vd.pop('on_invalid', None)
|
||||
|
||||
q = Q()
|
||||
if vd.get('name'):
|
||||
q |= Q(name=vd['name'])
|
||||
if vd.get('username'):
|
||||
q |= Q(username=vd['username'], secret_type=vd.get('secret_type'))
|
||||
|
||||
instance = Account.objects.filter(asset=vd['asset']).filter(q).first()
|
||||
# 不存在这个资产,不用关系策略
|
||||
if not instance:
|
||||
instance = Account.objects.create(**vd)
|
||||
return instance, 'created'
|
||||
|
||||
if on_invalid == AccountInvalidPolicy.SKIP:
|
||||
return instance, 'skipped'
|
||||
elif on_invalid == AccountInvalidPolicy.UPDATE:
|
||||
for k, v in vd.items():
|
||||
setattr(instance, k, v)
|
||||
instance.save()
|
||||
return instance, 'updated'
|
||||
else:
|
||||
raise serializers.ValidationError('Account already exists')
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
return self.set_secret(attrs)
|
||||
if self.instance:
|
||||
return attrs
|
||||
|
||||
@staticmethod
|
||||
def push_account(instance, push_now):
|
||||
if not push_now:
|
||||
return
|
||||
push_accounts_to_assets_task.delay([str(instance.id)])
|
||||
template = attrs.pop('template', None)
|
||||
if template:
|
||||
attrs['source'] = Source.TEMPLATE
|
||||
attrs['source_id'] = str(template.id)
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
instance = super().create(validated_data)
|
||||
self.push_account(instance, push_now)
|
||||
params = validated_data.pop('params', None)
|
||||
instance, stat = self.do_create(validated_data)
|
||||
self.push_account_if_need(instance, push_now, params, stat)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# account cannot be modified
|
||||
validated_data.pop('username', None)
|
||||
validated_data.pop('on_invalid', None)
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
params = validated_data.pop('params', None)
|
||||
validated_data['source_id'] = None
|
||||
instance = super().update(instance, validated_data)
|
||||
self.push_account(instance, push_now)
|
||||
self.push_account_if_need(instance, push_now, params, 'updated')
|
||||
return instance
|
||||
|
||||
|
||||
class AccountSerializerCreateMixin(AccountSerializerCreateValidateMixin, BulkModelSerializer):
|
||||
template = serializers.BooleanField(
|
||||
default=False, label=_("Template"), write_only=True
|
||||
)
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
|
||||
|
||||
class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
platform = ObjectRelatedField(read_only=True)
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
|
@ -77,11 +174,11 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info']
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, dict):
|
||||
i = data.get('id')
|
||||
i = data.get('id') or data.get('pk')
|
||||
else:
|
||||
i = data
|
||||
|
||||
|
@ -91,9 +188,10 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
|||
raise serializers.ValidationError(_('Asset not found'))
|
||||
|
||||
|
||||
class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
|
||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||
asset = AccountAssetSerializer(label=_('Asset'))
|
||||
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
su_from = ObjectRelatedField(
|
||||
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
|
||||
label=_('Su from'), attrs=('id', 'name', 'username')
|
||||
|
@ -102,27 +200,179 @@ class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
|
|||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = Account
|
||||
fields = BaseAccountSerializer.Meta.fields + [
|
||||
'su_from', 'asset', 'template', 'version',
|
||||
'push_now', 'source', 'connectivity',
|
||||
'su_from', 'asset', 'version',
|
||||
'source', 'source_id', 'connectivity',
|
||||
] + AccountCreateUpdateSerializerMixin.Meta.fields
|
||||
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
|
||||
'source', 'source_id', 'connectivity'
|
||||
]
|
||||
extra_kwargs = {
|
||||
**BaseAccountSerializer.Meta.extra_kwargs,
|
||||
'name': {'required': False, 'allow_null': True},
|
||||
'name': {'required': False},
|
||||
}
|
||||
|
||||
def validate_name(self, value):
|
||||
if not value:
|
||||
value = self.initial_data.get('username')
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset \
|
||||
.prefetch_related('asset', 'asset__platform', 'asset__platform__automation')
|
||||
queryset = queryset.prefetch_related(
|
||||
'asset', 'asset__platform',
|
||||
'asset__platform__automation'
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||
state = serializers.CharField(read_only=True, label=_('State'))
|
||||
error = serializers.CharField(read_only=True, label=_('Error'))
|
||||
changed = serializers.BooleanField(read_only=True, label=_('Changed'))
|
||||
|
||||
|
||||
class AssetAccountBulkSerializer(AccountCreateUpdateSerializerMixin, serializers.ModelSerializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'name', 'username', 'secret', 'secret_type',
|
||||
'privileged', 'is_active', 'comment', 'template',
|
||||
'on_invalid', 'push_now', 'assets',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
'secret_type': {'required': False},
|
||||
}
|
||||
|
||||
def set_initial_value(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
return
|
||||
initial_data = self.initial_data
|
||||
self.from_template_if_need(initial_data)
|
||||
|
||||
@staticmethod
|
||||
def get_filter_lookup(vd):
|
||||
return {
|
||||
'username': vd['username'],
|
||||
'secret_type': vd['secret_type'],
|
||||
'asset': vd['asset'],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_uniq_name(vd):
|
||||
return vd['name'] + '-' + uuid.uuid4().hex[:4]
|
||||
|
||||
@staticmethod
|
||||
def _handle_update_create(vd, lookup):
|
||||
ori = Account.objects.filter(**lookup).first()
|
||||
if ori and ori.secret == vd['secret']:
|
||||
return ori, False, 'skipped'
|
||||
|
||||
instance, value = Account.objects.update_or_create(defaults=vd, **lookup)
|
||||
state = 'created' if value else 'updated'
|
||||
return instance, True, state
|
||||
|
||||
@staticmethod
|
||||
def _handle_skip_create(vd, lookup):
|
||||
instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
|
||||
state = 'created' if value else 'skipped'
|
||||
return instance, value, state
|
||||
|
||||
@staticmethod
|
||||
def _handle_err_create(vd, lookup):
|
||||
instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
|
||||
if not value:
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
return instance, True, 'created'
|
||||
|
||||
def perform_create(self, vd, handler):
|
||||
lookup = self.get_filter_lookup(vd)
|
||||
try:
|
||||
instance, changed, state = handler(vd, lookup)
|
||||
except IntegrityError:
|
||||
vd['name'] = self.get_uniq_name(vd)
|
||||
instance, changed, state = handler(vd, lookup)
|
||||
return instance, changed, state
|
||||
|
||||
def get_create_handler(self, on_invalid):
|
||||
if on_invalid == 'update':
|
||||
handler = self._handle_update_create
|
||||
elif on_invalid == 'skip':
|
||||
handler = self._handle_skip_create
|
||||
else:
|
||||
handler = self._handle_err_create
|
||||
return handler
|
||||
|
||||
def perform_bulk_create(self, vd):
|
||||
assets = vd.pop('assets')
|
||||
on_invalid = vd.pop('on_invalid', 'skip')
|
||||
secret_type = vd.get('secret_type', 'password')
|
||||
|
||||
if not vd.get('name'):
|
||||
vd['name'] = vd.get('username')
|
||||
|
||||
create_handler = self.get_create_handler(on_invalid)
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
|
||||
|
||||
_results = {}
|
||||
for asset in assets:
|
||||
if asset not in secret_type_supports:
|
||||
_results[asset] = {
|
||||
'error': _('Asset does not support this secret type: %s') % secret_type,
|
||||
'state': 'error',
|
||||
}
|
||||
continue
|
||||
|
||||
vd = vd.copy()
|
||||
vd['asset'] = asset
|
||||
try:
|
||||
instance, changed, state = self.perform_create(vd, create_handler)
|
||||
_results[asset] = {
|
||||
'changed': changed, 'instance': instance.id, 'state': state
|
||||
}
|
||||
except serializers.ValidationError as e:
|
||||
_results[asset] = {'error': e.detail[0], 'state': 'error'}
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
_results[asset] = {'error': str(e), 'state': 'error'}
|
||||
|
||||
results = [{'asset': asset, **result} for asset, result in _results.items()]
|
||||
state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
|
||||
results = sorted(results, key=lambda x: state_score.get(x['state'], 4))
|
||||
|
||||
if on_invalid != 'error':
|
||||
return results
|
||||
|
||||
errors = []
|
||||
errors.extend([result for result in results if result['state'] == 'error'])
|
||||
for result in results:
|
||||
if result['state'] != 'skipped':
|
||||
continue
|
||||
errors.append({
|
||||
'error': _('Account has exist'),
|
||||
'state': 'error',
|
||||
'asset': str(result['asset'])
|
||||
})
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def push_accounts_if_need(results, push_now):
|
||||
if not push_now:
|
||||
return
|
||||
accounts = [str(v['instance']) for v in results if v.get('instance')]
|
||||
push_accounts_to_assets_task.delay(accounts)
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', False)
|
||||
results = self.perform_bulk_create(validated_data)
|
||||
self.push_accounts_if_need(results, push_now)
|
||||
for res in results:
|
||||
res['asset'] = str(res['asset'])
|
||||
return results
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
|
|
|
@ -13,10 +13,10 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
|
|||
|
||||
class AuthValidateMixin(serializers.Serializer):
|
||||
secret_type = LabeledChoiceField(
|
||||
choices=SecretType.choices, required=True, label=_('Secret type')
|
||||
choices=SecretType.choices, label=_('Secret type'), default='password'
|
||||
)
|
||||
secret = EncryptedField(
|
||||
label=_('Secret/Password'), required=False, max_length=40960, allow_blank=True,
|
||||
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
||||
allow_null=True, write_only=True,
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
|
@ -77,6 +77,5 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
|||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
'spec_info': {'label': _('Spec info')},
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from accounts.models import AccountTemplate
|
||||
from accounts.models import AccountTemplate, Account
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
|
@ -7,17 +8,47 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
|||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = AccountTemplate
|
||||
|
||||
# @classmethod
|
||||
# def validate_required(cls, attrs):
|
||||
# # TODO 选择模版后检查一些必填项
|
||||
# required_field_dict = {}
|
||||
# error = _('This field is required.')
|
||||
# for k, v in cls().fields.items():
|
||||
# if v.required and k not in attrs:
|
||||
# required_field_dict[k] = error
|
||||
# if not required_field_dict:
|
||||
# return
|
||||
# raise serializers.ValidationError(required_field_dict)
|
||||
@staticmethod
|
||||
def bulk_update_accounts(instance, diff):
|
||||
accounts = Account.objects.filter(source_id=instance.id)
|
||||
if not accounts:
|
||||
return
|
||||
|
||||
secret_type = diff.pop('secret_type', None)
|
||||
diff.pop('secret', None)
|
||||
update_accounts = []
|
||||
for account in accounts:
|
||||
for field, value in diff.items():
|
||||
setattr(account, field, value)
|
||||
update_accounts.append(account)
|
||||
if update_accounts:
|
||||
Account.objects.bulk_update(update_accounts, diff.keys())
|
||||
|
||||
if secret_type is None:
|
||||
return
|
||||
|
||||
update_accounts = []
|
||||
asset_ids = accounts.values_list('asset_id', flat=True)
|
||||
secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
|
||||
asset_ids_supports = [asset.id for asset in secret_type_supports]
|
||||
for account in accounts:
|
||||
asset_id = account.asset_id
|
||||
if asset_id not in asset_ids_supports:
|
||||
continue
|
||||
account.secret_type = secret_type
|
||||
account.secret = instance.secret
|
||||
update_accounts.append(account)
|
||||
if update_accounts:
|
||||
Account.objects.bulk_update(update_accounts, ['secret', 'secret_type'])
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
diff = {
|
||||
k: v for k, v in validated_data.items()
|
||||
if getattr(instance, k) != v
|
||||
}
|
||||
instance = super().update(instance, validated_data)
|
||||
self.bulk_update_accounts(instance, diff)
|
||||
return instance
|
||||
|
||||
|
||||
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
|
||||
|
|
|
@ -17,7 +17,8 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
|||
class Meta:
|
||||
model = GatherAccountsAutomation
|
||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields + read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields \
|
||||
+ ['is_sync_account'] + read_only_fields
|
||||
|
||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||
|
||||
|
|
|
@ -7,9 +7,10 @@ from .change_secret import (
|
|||
|
||||
|
||||
class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
|
||||
|
||||
class Meta(ChangeSecretAutomationSerializer.Meta):
|
||||
model = PushAccountAutomation
|
||||
fields = [
|
||||
fields = ['params'] + [
|
||||
n for n in ChangeSecretAutomationSerializer.Meta.fields
|
||||
if n not in ['recipients']
|
||||
]
|
||||
|
|
|
@ -8,8 +8,8 @@ logger = get_logger(__name__)
|
|||
|
||||
|
||||
@receiver(pre_save, sender=Account)
|
||||
def on_account_pre_save(sender, instance, created=False, **kwargs):
|
||||
if created:
|
||||
def on_account_pre_save(sender, instance, **kwargs):
|
||||
if instance.version == 0:
|
||||
instance.version = 1
|
||||
else:
|
||||
instance.version = instance.history.count()
|
||||
|
|
|
@ -15,7 +15,7 @@ __all__ = [
|
|||
queue="ansible", verbose_name=_('Push accounts to assets'),
|
||||
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None)
|
||||
)
|
||||
def push_accounts_to_assets_task(account_ids):
|
||||
def push_accounts_to_assets_task(account_ids, params=None):
|
||||
from accounts.models import PushAccountAutomation
|
||||
from accounts.models import Account
|
||||
|
||||
|
@ -26,6 +26,7 @@ def push_accounts_to_assets_task(account_ids):
|
|||
task_snapshot = {
|
||||
'accounts': [str(account.id) for account in accounts],
|
||||
'assets': [str(account.asset_id) for account in accounts],
|
||||
'params': params or {},
|
||||
}
|
||||
|
||||
tp = AutomationTypes.push_account
|
||||
|
|
|
@ -25,6 +25,7 @@ router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'pu
|
|||
router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record')
|
||||
|
||||
urlpatterns = [
|
||||
path('accounts/bulk/', api.AssetAccountBulkCreateApi.as_view(), name='account-bulk-create'),
|
||||
path('accounts/tasks/', api.AccountsTaskCreateAPI.as_view(), name='account-task-create'),
|
||||
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
|
||||
name='account-secret-history'),
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from .asset import *
|
||||
from .host import *
|
||||
from .database import *
|
||||
from .web import *
|
||||
from .cloud import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .host import *
|
||||
from .permission import *
|
||||
from .web import *
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
#
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
|
||||
from assets import serializers
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
|
||||
from assets.models import Asset, Gateway
|
||||
from assets.models import Asset, Gateway, Platform
|
||||
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
||||
from common.api import SuggestionMixin
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
@ -18,6 +20,7 @@ from common.utils import get_logger, is_uuid
|
|||
from orgs.mixins import generics
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..mixin import NodeFilterMixin
|
||||
from ...notifications import BulkUpdatePlatformSkipAssetUserMsg
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
|
@ -99,16 +102,16 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
("platform", serializers.PlatformSerializer),
|
||||
("suggestion", serializers.MiniAssetSerializer),
|
||||
("gateways", serializers.GatewaySerializer),
|
||||
("spec_info", serializers.SpecSerializer),
|
||||
)
|
||||
rbac_perms = (
|
||||
("match", "assets.match_asset"),
|
||||
("platform", "assets.view_platform"),
|
||||
("gateways", "assets.view_gateway"),
|
||||
("spec_info", "assets.view_asset"),
|
||||
("info", "assets.view_asset"),
|
||||
("gathered_info", "assets.view_asset"),
|
||||
)
|
||||
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
|
||||
skip_assets = []
|
||||
|
||||
def get_serializer_class(self):
|
||||
cls = super().get_serializer_class()
|
||||
|
@ -124,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
serializer = super().get_serializer(instance=asset.platform)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=["GET"], detail=True, url_path="spec-info")
|
||||
def spec_info(self, *args, **kwargs):
|
||||
asset = super().get_object()
|
||||
return Response(asset.spec_info)
|
||||
|
||||
@action(methods=["GET"], detail=True, url_path="gateways")
|
||||
def gateways(self, *args, **kwargs):
|
||||
asset = self.get_object()
|
||||
|
@ -144,6 +142,31 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
return Response({'error': error}, status=400)
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def filter_bulk_update_data(self):
|
||||
bulk_data = []
|
||||
for data in self.request.data:
|
||||
pk = data.get('id')
|
||||
platform = data.get('platform')
|
||||
if not platform:
|
||||
bulk_data.append(data)
|
||||
continue
|
||||
asset = get_object_or_404(Asset, pk=pk)
|
||||
platform = get_object_or_404(Platform, **platform)
|
||||
if platform.type == asset.type:
|
||||
bulk_data.append(data)
|
||||
continue
|
||||
self.skip_assets.append(asset)
|
||||
return bulk_data
|
||||
|
||||
def bulk_update(self, request, *args, **kwargs):
|
||||
bulk_data = self.filter_bulk_update_data()
|
||||
request._full_data = bulk_data
|
||||
response = super().bulk_update(request, *args, **kwargs)
|
||||
if response.status_code == HTTP_200_OK and self.skip_assets:
|
||||
user = request.user
|
||||
BulkUpdatePlatformSkipAssetUserMsg(user, self.skip_assets).publish()
|
||||
return response
|
||||
|
||||
|
||||
class AssetsTaskMixin:
|
||||
def perform_assets_task(self, serializer):
|
||||
|
@ -154,8 +177,8 @@ class AssetsTaskMixin:
|
|||
task = update_assets_hardware_info_manual(assets)
|
||||
else:
|
||||
asset = assets[0]
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = test_assets_connectivity_manual(assets)
|
||||
return task
|
||||
|
|
|
@ -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
|
|
@ -1,8 +1,5 @@
|
|||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from assets.models import Host, Asset
|
||||
from assets.serializers import HostSerializer, HostInfoSerializer
|
||||
from assets.serializers import HostSerializer
|
||||
from .asset import AssetViewSet
|
||||
|
||||
__all__ = ['HostViewSet']
|
||||
|
@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet):
|
|||
def get_serializer_classes(self):
|
||||
serializer_classes = super().get_serializer_classes()
|
||||
serializer_classes['default'] = HostSerializer
|
||||
serializer_classes['info'] = HostInfoSerializer
|
||||
return serializer_classes
|
||||
|
||||
@action(methods=["GET"], detail=True, url_path="info")
|
||||
def info(self, *args, **kwargs):
|
||||
asset = super().get_object()
|
||||
serializer = self.get_serializer(asset.info)
|
||||
data = serializer.data
|
||||
data['asset'] = {
|
||||
'id': asset.id, 'name': asset.name,
|
||||
'address': asset.address
|
||||
}
|
||||
return Response(data)
|
||||
|
|
|
@ -8,6 +8,15 @@ from common.utils import lazyproperty, timeit
|
|||
|
||||
|
||||
class SerializeToTreeNodeMixin:
|
||||
request: Request
|
||||
|
||||
@lazyproperty
|
||||
def is_sync(self):
|
||||
sync_paths = ['/api/v1/perms/users/self/nodes/all-with-assets/tree/']
|
||||
for p in sync_paths:
|
||||
if p == self.request.path:
|
||||
return True
|
||||
return False
|
||||
|
||||
@timeit
|
||||
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
|
||||
|
@ -17,6 +26,16 @@ class SerializeToTreeNodeMixin:
|
|||
else:
|
||||
def _name(node: Node):
|
||||
return node.value
|
||||
|
||||
def _open(node):
|
||||
if not self.is_sync:
|
||||
# 异步加载资产树时,默认展开节点
|
||||
return True
|
||||
if not node.parent_key:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
data = [
|
||||
{
|
||||
'id': node.key,
|
||||
|
@ -24,7 +43,7 @@ class SerializeToTreeNodeMixin:
|
|||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'open': True,
|
||||
'open': _open(node),
|
||||
'meta': {
|
||||
'data': {
|
||||
"id": node.id,
|
||||
|
@ -52,7 +71,7 @@ class SerializeToTreeNodeMixin:
|
|||
{
|
||||
'id': str(asset.id),
|
||||
'name': asset.name,
|
||||
'title': asset.address,
|
||||
'title': f'{asset.address}\n{asset.comment}',
|
||||
'pId': get_pid(asset),
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
|
@ -64,6 +83,8 @@ class SerializeToTreeNodeMixin:
|
|||
'platform_type': asset.platform.type,
|
||||
'org_name': asset.org_name,
|
||||
'sftp': asset.platform_id in sftp_enabled_platform,
|
||||
'name': asset.name,
|
||||
'address': asset.address
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
from rest_framework import generics
|
||||
from rest_framework import serializers
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from assets.const import AllTypes
|
||||
from assets.models import Platform
|
||||
from assets.models import Platform, Node, Asset
|
||||
from assets.serializers import PlatformSerializer
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidUser
|
||||
from common.serializers import GroupedChoiceSerializer
|
||||
|
||||
__all__ = ['AssetPlatformViewSet']
|
||||
__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi']
|
||||
|
||||
|
||||
class AssetPlatformViewSet(JMSModelViewSet):
|
||||
|
@ -18,12 +24,13 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
|||
rbac_perms = {
|
||||
'categories': 'assets.view_platform',
|
||||
'type_constraints': 'assets.view_platform',
|
||||
'ops_methods': 'assets.view_platform'
|
||||
'ops_methods': 'assets.view_platform',
|
||||
'filter_nodes_assets': 'assets.view_platform'
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type__in=AllTypes.get_types())
|
||||
queryset = queryset.filter(type__in=AllTypes.get_types_values())
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
|
@ -38,3 +45,44 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
|||
request, message={"detail": "Internal platform"}
|
||||
)
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='filter-nodes-assets')
|
||||
def filter_nodes_assets(self, request, *args, **kwargs):
|
||||
node_ids = request.data.get('node_ids', [])
|
||||
asset_ids = request.data.get('asset_ids', [])
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
direct_asset_ids = Asset.objects.filter(id__in=asset_ids).values_list('id', flat=True)
|
||||
platform_ids = Asset.objects.filter(
|
||||
id__in=set(list(direct_asset_ids) + list(node_asset_ids))
|
||||
).values_list('platform_id', flat=True)
|
||||
platforms = Platform.objects.filter(id__in=platform_ids)
|
||||
serializer = self.get_serializer(platforms, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class PlatformAutomationMethodsApi(generics.ListAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
|
||||
@staticmethod
|
||||
def automation_methods():
|
||||
return AllTypes.get_automation_methods()
|
||||
|
||||
def generate_serializer_fields(self):
|
||||
data = self.automation_methods()
|
||||
fields = {
|
||||
i['id']: i['params_serializer']()
|
||||
if i['params_serializer'] else None
|
||||
for i in data
|
||||
}
|
||||
return fields
|
||||
|
||||
def get_serializer_class(self):
|
||||
fields = self.generate_serializer_fields()
|
||||
serializer_name = 'AutomationMethodsSerializer'
|
||||
return type(serializer_name, (serializers.Serializer,), fields)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
data = self.generate_serializer_fields()
|
||||
serializer = self.get_serializer(data)
|
||||
return Response(serializer.data)
|
||||
|
|
|
@ -41,6 +41,26 @@ class BasePlaybookManager:
|
|||
self.method_hosts_mapper = defaultdict(list)
|
||||
self.playbooks = []
|
||||
self.gateway_servers = dict()
|
||||
params = self.execution.snapshot.get('params')
|
||||
self.params = params or {}
|
||||
|
||||
def get_params(self, automation, method_type):
|
||||
method_attr = '{}_method'.format(method_type)
|
||||
method_params = '{}_params'.format(method_type)
|
||||
method_id = getattr(automation, method_attr)
|
||||
automation_params = getattr(automation, method_params)
|
||||
serializer = self.method_id_meta_mapper[method_id]['params_serializer']
|
||||
|
||||
if serializer is None:
|
||||
return {}
|
||||
|
||||
data = self.params.get(method_id, {})
|
||||
params = serializer(data).data
|
||||
return {
|
||||
field_name: automation_params.get(field_name, '')
|
||||
if not params[field_name] else params[field_name]
|
||||
for field_name in params
|
||||
}
|
||||
|
||||
@property
|
||||
def platform_automation_methods(self):
|
||||
|
@ -101,8 +121,9 @@ class BasePlaybookManager:
|
|||
return host
|
||||
|
||||
def host_callback(self, host, automation=None, **kwargs):
|
||||
enabled_attr = '{}_enabled'.format(self.__class__.method_type())
|
||||
method_attr = '{}_method'.format(self.__class__.method_type())
|
||||
method_type = self.__class__.method_type()
|
||||
enabled_attr = '{}_enabled'.format(method_type)
|
||||
method_attr = '{}_method'.format(method_type)
|
||||
|
||||
method_enabled = automation and \
|
||||
getattr(automation, enabled_attr) and \
|
||||
|
@ -114,6 +135,7 @@ class BasePlaybookManager:
|
|||
return host
|
||||
|
||||
host = self.convert_cert_to_file(host, kwargs.get('path_dir'))
|
||||
host['params'] = self.get_params(automation, method_type)
|
||||
return host
|
||||
|
||||
@staticmethod
|
||||
|
@ -239,10 +261,12 @@ class BasePlaybookManager:
|
|||
jms_asset, jms_gateway = host['jms_asset'], host.get('gateway')
|
||||
if not jms_gateway:
|
||||
continue
|
||||
|
||||
server = SSHTunnelForwarder(
|
||||
(jms_gateway['address'], jms_gateway['port']),
|
||||
ssh_username=jms_gateway['username'],
|
||||
ssh_password=jms_gateway['secret'],
|
||||
ssh_pkey=jms_gateway['private_key_path'],
|
||||
remote_bind_address=(jms_asset['address'], jms_asset['port'])
|
||||
)
|
||||
try:
|
||||
|
@ -252,8 +276,8 @@ class BasePlaybookManager:
|
|||
print('\033[31m %s \033[0m\n' % err_msg)
|
||||
not_valid.append(k)
|
||||
else:
|
||||
jms_asset['address'] = '127.0.0.1'
|
||||
jms_asset['port'] = server.local_bind_port
|
||||
host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
|
||||
host['ansible_port'] = jms_asset['port'] = server.local_bind_port
|
||||
servers.append(server)
|
||||
|
||||
# 网域不可连接的,就不继续执行此资源的后续任务了
|
||||
|
|
|
@ -29,7 +29,7 @@ class GatherFactsManager(BasePlaybookManager):
|
|||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
info = self.format_asset_info(asset.type, info)
|
||||
asset.info = info
|
||||
asset.save(update_fields=['info'])
|
||||
asset.gathered_info = info
|
||||
asset.save(update_fields=['gathered_info'])
|
||||
else:
|
||||
logger.error("Not found info: {}".format(host))
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import yaml
|
||||
import json
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def check_platform_method(manifest, manifest_path):
|
||||
required_keys = ['category', 'method', 'name', 'id', 'type']
|
||||
|
@ -21,6 +22,15 @@ def check_platform_methods(methods):
|
|||
raise ValueError("Duplicate id: {}".format(_id))
|
||||
|
||||
|
||||
def generate_serializer(data):
|
||||
from common.serializers import create_serializer_class
|
||||
params = data.pop('params', None)
|
||||
if not params:
|
||||
return None
|
||||
serializer_name = data['id'].title().replace('_', '') + 'Serializer'
|
||||
return create_serializer_class(serializer_name, params)
|
||||
|
||||
|
||||
def get_platform_automation_methods(path):
|
||||
methods = []
|
||||
for root, dirs, files in os.walk(path, topdown=False):
|
||||
|
@ -33,6 +43,7 @@ def get_platform_automation_methods(path):
|
|||
manifest = yaml.safe_load(f)
|
||||
check_platform_method(manifest, path)
|
||||
manifest['dir'] = os.path.dirname(path)
|
||||
manifest['params_serializer'] = generate_serializer(manifest)
|
||||
methods.append(manifest)
|
||||
|
||||
check_platform_methods(methods)
|
||||
|
@ -46,12 +57,12 @@ def filter_key(manifest, attr, value):
|
|||
return value in manifest_value or 'all' in manifest_value
|
||||
|
||||
|
||||
def filter_platform_methods(category, tp, method=None, methods=None):
|
||||
def filter_platform_methods(category, tp_name, method=None, methods=None):
|
||||
methods = platform_automation_methods if methods is None else methods
|
||||
if category:
|
||||
methods = filter(partial(filter_key, attr='category', value=category), methods)
|
||||
if tp:
|
||||
methods = filter(partial(filter_key, attr='type', value=tp), methods)
|
||||
if tp_name:
|
||||
methods = filter(partial(filter_key, attr='type', value=tp_name), methods)
|
||||
if method:
|
||||
methods = filter(lambda x: x['method'] == method, methods)
|
||||
return methods
|
||||
|
|
|
@ -4,10 +4,21 @@ from jumpserver.utils import has_valid_xpack_license
|
|||
from .protocol import Protocol
|
||||
|
||||
|
||||
class Type:
|
||||
def __init__(self, label, value):
|
||||
self.label = label
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class BaseType(TextChoices):
|
||||
"""
|
||||
约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持
|
||||
约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh,
|
||||
或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_constrains(cls):
|
||||
constrains = {}
|
||||
|
@ -20,7 +31,7 @@ class BaseType(TextChoices):
|
|||
protocols_default = protocols.pop('*', {})
|
||||
automation_default = automation.pop('*', {})
|
||||
|
||||
for k, v in cls.choices:
|
||||
for k, v in cls.get_choices():
|
||||
tp_base = {**base_default, **base.get(k, {})}
|
||||
tp_auto = {**automation_default, **automation.get(k, {})}
|
||||
tp_protocols = {**protocols_default, **protocols.get(k, {})}
|
||||
|
@ -35,8 +46,12 @@ class BaseType(TextChoices):
|
|||
choices = protocol.get('choices', [])
|
||||
if choices == '__self__':
|
||||
choices = [tp]
|
||||
protocols = [{'name': name, **settings.get(name, {})} for name in choices]
|
||||
protocols[0]['primary'] = True
|
||||
protocols = [
|
||||
{'name': name, **settings.get(name, {})}
|
||||
for name in choices
|
||||
]
|
||||
if protocols:
|
||||
protocols[0]['default'] = True
|
||||
return protocols
|
||||
|
||||
@classmethod
|
||||
|
@ -56,23 +71,21 @@ class BaseType(TextChoices):
|
|||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_community_types(cls):
|
||||
raise NotImplementedError
|
||||
def _get_choices_to_types(cls):
|
||||
choices = cls.get_choices()
|
||||
return [Type(label, value) for value, label in choices]
|
||||
|
||||
@classmethod
|
||||
def get_types(cls):
|
||||
tps = [tp for tp in cls]
|
||||
tps = cls._get_choices_to_types()
|
||||
if not has_valid_xpack_license():
|
||||
tps = cls.get_community_types()
|
||||
return tps
|
||||
|
||||
@classmethod
|
||||
def get_community_types(cls):
|
||||
return cls._get_choices_to_types()
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
tps = cls.get_types()
|
||||
cls_choices = cls.choices
|
||||
return [
|
||||
choice for choice in cls_choices
|
||||
if choice[0] in tps
|
||||
]
|
||||
|
||||
|
||||
return cls.choices
|
||||
|
|
|
@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from common.db.models import ChoicesMixin
|
||||
|
||||
|
||||
__all__ = ['Category']
|
||||
|
||||
|
||||
|
@ -13,13 +12,10 @@ class Category(ChoicesMixin, models.TextChoices):
|
|||
DATABASE = 'database', _("Database")
|
||||
CLOUD = 'cloud', _("Cloud service")
|
||||
WEB = 'web', _("Web")
|
||||
CUSTOM = 'custom', _("Custom type")
|
||||
|
||||
@classmethod
|
||||
def filter_choices(cls, category):
|
||||
_category = getattr(cls, category.upper(), None)
|
||||
choices = [(_category.value, _category.label)] if _category else cls.choices
|
||||
return choices
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@ class CloudTypes(BaseType):
|
|||
'charset_enabled': False,
|
||||
'domain_enabled': False,
|
||||
'su_enabled': False,
|
||||
},
|
||||
cls.K8S: {
|
||||
'domain_enabled': True,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
|
@ -15,7 +15,8 @@ class DeviceTypes(BaseType):
|
|||
'*': {
|
||||
'charset_enabled': False,
|
||||
'domain_enabled': True,
|
||||
'su_enabled': False,
|
||||
'su_enabled': True,
|
||||
'su_methods': ['enable', 'super', 'super_level']
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,7 @@ class HostTypes(BaseType):
|
|||
'charset': 'utf-8', # default
|
||||
'domain_enabled': True,
|
||||
'su_enabled': True,
|
||||
'su_methods': [
|
||||
{'name': 'sudo su', 'id': 'sudo su'},
|
||||
{'name': 'su -', 'id': 'su -'}
|
||||
],
|
||||
'su_methods': ['sudo', 'su'],
|
||||
},
|
||||
cls.WINDOWS: {
|
||||
'su_enabled': False,
|
||||
|
@ -39,7 +36,7 @@ class HostTypes(BaseType):
|
|||
'choices': ['ssh', 'telnet', 'vnc', 'rdp']
|
||||
},
|
||||
cls.WINDOWS: {
|
||||
'choices': ['rdp', 'ssh', 'vnc']
|
||||
'choices': ['rdp', 'ssh', 'vnc', 'winrm']
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +58,7 @@ class HostTypes(BaseType):
|
|||
cls.WINDOWS: {
|
||||
'ansible_config': {
|
||||
'ansible_shell_type': 'cmd',
|
||||
'ansible_connection': 'ssh',
|
||||
'ansible_connection': 'smart',
|
||||
},
|
||||
},
|
||||
cls.OTHER_HOST: {
|
||||
|
|
|
@ -10,6 +10,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
vnc = 'vnc', 'VNC'
|
||||
winrm = 'winrm', 'WinRM'
|
||||
|
||||
mysql = 'mysql', 'MySQL'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
|
@ -51,6 +52,13 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||
'port': 23,
|
||||
'secret_types': ['password'],
|
||||
},
|
||||
cls.winrm: {
|
||||
'port': 5985,
|
||||
'secret_types': ['password'],
|
||||
'setting': {
|
||||
'use_ssl': False,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -116,7 +124,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||
'setting': {
|
||||
'username_selector': 'name=username',
|
||||
'password_selector': 'name=password',
|
||||
'submit_selector': 'id=longin_button',
|
||||
'submit_selector': 'id=login_button',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -128,3 +136,11 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||
**cls.database_protocols(),
|
||||
**cls.cloud_protocols()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def protocol_secret_types(cls):
|
||||
settings = cls.settings()
|
||||
return {
|
||||
protocol: settings[protocol]['secret_types'] or ['password']
|
||||
for protocol in cls.settings()
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
|
|||
from common.db.models import ChoicesMixin
|
||||
from .category import Category
|
||||
from .cloud import CloudTypes
|
||||
from .custom import CustomTypes
|
||||
from .database import DatabaseTypes
|
||||
from .device import DeviceTypes
|
||||
from .host import HostTypes
|
||||
|
@ -16,7 +17,7 @@ class AllTypes(ChoicesMixin):
|
|||
choices: list
|
||||
includes = [
|
||||
HostTypes, DeviceTypes, DatabaseTypes,
|
||||
CloudTypes, WebTypes,
|
||||
CloudTypes, WebTypes, CustomTypes
|
||||
]
|
||||
_category_constrains = {}
|
||||
|
||||
|
@ -24,22 +25,29 @@ class AllTypes(ChoicesMixin):
|
|||
def choices(cls):
|
||||
choices = []
|
||||
for tp in cls.includes:
|
||||
choices.extend(tp.choices)
|
||||
choices.extend(tp.get_choices())
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
return cls.choices()
|
||||
|
||||
@classmethod
|
||||
def filter_choices(cls, category):
|
||||
choices = dict(cls.category_types()).get(category, cls).choices
|
||||
choices = dict(cls.category_types()).get(category, cls).get_choices()
|
||||
return choices() if callable(choices) else choices
|
||||
|
||||
@classmethod
|
||||
def get_constraints(cls, category, tp):
|
||||
def get_constraints(cls, category, tp_name):
|
||||
if not isinstance(tp_name, str):
|
||||
tp_name = tp_name.value
|
||||
|
||||
types_cls = dict(cls.category_types()).get(category)
|
||||
if not types_cls:
|
||||
return {}
|
||||
type_constraints = types_cls.get_constrains()
|
||||
constraints = type_constraints.get(tp, {})
|
||||
cls.set_automation_methods(category, tp, constraints)
|
||||
constraints = type_constraints.get(tp_name, {})
|
||||
cls.set_automation_methods(category, tp_name, constraints)
|
||||
return constraints
|
||||
|
||||
@classmethod
|
||||
|
@ -56,7 +64,7 @@ class AllTypes(ChoicesMixin):
|
|||
return asset_methods + account_methods
|
||||
|
||||
@classmethod
|
||||
def set_automation_methods(cls, category, tp, constraints):
|
||||
def set_automation_methods(cls, category, tp_name, constraints):
|
||||
from assets.automations import filter_platform_methods
|
||||
automation = constraints.get('automation', {})
|
||||
automation_methods = {}
|
||||
|
@ -66,7 +74,7 @@ class AllTypes(ChoicesMixin):
|
|||
continue
|
||||
item_name = item.replace('_enabled', '')
|
||||
methods = filter_platform_methods(
|
||||
category, tp, item_name, methods=platform_automation_methods
|
||||
category, tp_name, item_name, methods=platform_automation_methods
|
||||
)
|
||||
methods = [{'name': m['name'], 'id': m['id']} for m in methods]
|
||||
automation_methods[item_name + '_methods'] = methods
|
||||
|
@ -113,7 +121,7 @@ class AllTypes(ChoicesMixin):
|
|||
|
||||
@classmethod
|
||||
def grouped_choices(cls):
|
||||
grouped_types = [(str(ca), tp.choices) for ca, tp in cls.category_types()]
|
||||
grouped_types = [(str(ca), tp.get_choices()) for ca, tp in cls.category_types()]
|
||||
return grouped_types
|
||||
|
||||
@classmethod
|
||||
|
@ -136,16 +144,22 @@ class AllTypes(ChoicesMixin):
|
|||
(Category.HOST, HostTypes),
|
||||
(Category.DEVICE, DeviceTypes),
|
||||
(Category.DATABASE, DatabaseTypes),
|
||||
(Category.CLOUD, CloudTypes),
|
||||
(Category.WEB, WebTypes),
|
||||
(Category.CLOUD, CloudTypes)
|
||||
(Category.CUSTOM, CustomTypes),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_types(cls):
|
||||
tps = []
|
||||
choices = []
|
||||
for i in dict(cls.category_types()).values():
|
||||
tps.extend(i.get_types())
|
||||
return tps
|
||||
choices.extend(i.get_types())
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_types_values(cls):
|
||||
choices = cls.get_types()
|
||||
return [c.value for c in choices]
|
||||
|
||||
@staticmethod
|
||||
def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None):
|
||||
|
|
|
@ -49,7 +49,10 @@ def migrate_asset_accounts(apps, schema_editor):
|
|||
account_values.update(auth_book_auth)
|
||||
|
||||
auth_infos = []
|
||||
username = account_values['username']
|
||||
username = account_values.get('username')
|
||||
if not username:
|
||||
continue
|
||||
|
||||
for attr in auth_attrs:
|
||||
secret = account_values.pop(attr, None)
|
||||
if not secret:
|
||||
|
|
|
@ -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),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
]
|
|
@ -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)
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,7 @@
|
|||
from .cloud import *
|
||||
from .common import *
|
||||
from .host import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .host import *
|
||||
from .web import *
|
||||
from .cloud import *
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db import models
|
||||
from django.forms import model_to_dict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from assets import const
|
||||
|
@ -94,6 +94,20 @@ class Protocol(models.Model):
|
|||
def __str__(self):
|
||||
return '{}/{}'.format(self.name, self.port)
|
||||
|
||||
@lazyproperty
|
||||
def asset_platform_protocol(self):
|
||||
protocols = self.asset.platform.protocols.values('name', 'public', 'setting')
|
||||
protocols = list(filter(lambda p: p['name'] == self.name, protocols))
|
||||
return protocols[0] if len(protocols) > 0 else {}
|
||||
|
||||
@property
|
||||
def setting(self):
|
||||
return self.asset_platform_protocol.get('setting', {})
|
||||
|
||||
@property
|
||||
def public(self):
|
||||
return self.asset_platform_protocol.get('public', True)
|
||||
|
||||
|
||||
class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
||||
Category = const.Category
|
||||
|
@ -108,7 +122,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
|||
verbose_name=_("Nodes"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
info = models.JSONField(verbose_name=_('Info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
|
||||
|
||||
objects = AssetManager.from_queryset(AssetQuerySet)()
|
||||
|
||||
|
@ -148,20 +163,27 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
|||
return self.get_spec_values(instance, spec_fields)
|
||||
|
||||
@lazyproperty
|
||||
def auto_info(self):
|
||||
def info(self):
|
||||
info = {}
|
||||
info.update(self.gathered_info or {})
|
||||
info.update(self.custom_info or {})
|
||||
info.update(self.spec_info or {})
|
||||
return info
|
||||
|
||||
@lazyproperty
|
||||
def auto_config(self):
|
||||
platform = self.platform
|
||||
automation = self.platform.automation
|
||||
return {
|
||||
auto_config = {
|
||||
'su_enabled': platform.su_enabled,
|
||||
'ping_enabled': automation.ping_enabled,
|
||||
'domain_enabled': platform.domain_enabled,
|
||||
'ansible_enabled': automation.ansible_enabled,
|
||||
'push_account_enabled': automation.push_account_enabled,
|
||||
'gather_facts_enabled': automation.gather_facts_enabled,
|
||||
'change_secret_enabled': automation.change_secret_enabled,
|
||||
'verify_account_enabled': automation.verify_account_enabled,
|
||||
'gather_accounts_enabled': automation.gather_accounts_enabled,
|
||||
'ansible_enabled': False
|
||||
}
|
||||
if not automation:
|
||||
return auto_config
|
||||
|
||||
auto_config.update(model_to_dict(automation))
|
||||
return auto_config
|
||||
|
||||
def get_target_ip(self):
|
||||
return self.address
|
||||
|
@ -191,25 +213,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
|||
names.append(n.name + ':' + n.value)
|
||||
return names
|
||||
|
||||
@lazyproperty
|
||||
def primary_protocol(self):
|
||||
from assets.const.types import AllTypes
|
||||
primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type)
|
||||
protocol = self.protocols.filter(name=primary_protocol_name).first()
|
||||
return protocol
|
||||
|
||||
@lazyproperty
|
||||
def protocol(self):
|
||||
if not self.primary_protocol:
|
||||
return 'none'
|
||||
return self.primary_protocol.name
|
||||
|
||||
@lazyproperty
|
||||
def port(self):
|
||||
if not self.primary_protocol:
|
||||
return 0
|
||||
return self.primary_protocol.port
|
||||
|
||||
@lazyproperty
|
||||
def type(self):
|
||||
return self.platform.type
|
||||
|
@ -275,6 +278,22 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
|||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
@staticmethod
|
||||
def get_secret_type_assets(asset_ids, secret_type):
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
asset_protocol = assets.prefetch_related('protocols').values_list('id', 'protocols__name')
|
||||
protocol_secret_types_map = const.Protocol.protocol_secret_types()
|
||||
asset_secret_types_mapp = defaultdict(set)
|
||||
|
||||
for asset_id, protocol in asset_protocol:
|
||||
secret_types = set(protocol_secret_types_map.get(protocol, []))
|
||||
asset_secret_types_mapp[asset_id].update(secret_types)
|
||||
|
||||
return [
|
||||
asset for asset in assets
|
||||
if secret_type in asset_secret_types_mapp.get(asset.id, [])
|
||||
]
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name')]
|
||||
verbose_name = _("Asset")
|
||||
|
|
|
@ -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")
|
|
@ -19,6 +19,7 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
type = models.CharField(max_length=16, verbose_name=_('Type'))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
params = models.JSONField(default=dict, verbose_name=_("Params"))
|
||||
|
||||
def __str__(self):
|
||||
return self.name + '@' + str(self.created_by)
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
from assets.models.platform import Platform
|
||||
from assets.const import GATEWAY_NAME
|
||||
from assets.models.platform import Platform
|
||||
from common.utils import get_logger, lazyproperty
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
from .asset.host import Host
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -57,6 +56,14 @@ class Gateway(Host):
|
|||
account = self.select_account
|
||||
return account.password if account else None
|
||||
|
||||
@lazyproperty
|
||||
def port(self):
|
||||
protocol = self.protocols.filter(name='ssh').first()
|
||||
if protocol:
|
||||
return protocol.port
|
||||
else:
|
||||
return '22'
|
||||
|
||||
@lazyproperty
|
||||
def private_key(self):
|
||||
account = self.select_account
|
||||
|
|
|
@ -10,56 +10,68 @@ __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
|
|||
|
||||
|
||||
class PlatformProtocol(models.Model):
|
||||
SETTING_ATTRS = {
|
||||
'console': False,
|
||||
'security': 'any,tls,rdp',
|
||||
'sftp_enabled': True,
|
||||
'sftp_home': '/tmp'
|
||||
}
|
||||
default = models.BooleanField(default=False, verbose_name=_('Default'))
|
||||
required = models.BooleanField(default=False, verbose_name=_('Required'))
|
||||
name = models.CharField(max_length=32, verbose_name=_('Name'))
|
||||
port = models.IntegerField(verbose_name=_('Port'))
|
||||
primary = models.BooleanField(default=False, verbose_name=_('Primary'))
|
||||
required = models.BooleanField(default=False, verbose_name=_('Required'))
|
||||
default = models.BooleanField(default=False, verbose_name=_('Default'))
|
||||
public = models.BooleanField(default=True, verbose_name=_('Public'))
|
||||
setting = models.JSONField(verbose_name=_('Setting'), default=dict)
|
||||
platform = models.ForeignKey('Platform', on_delete=models.CASCADE, related_name='protocols')
|
||||
|
||||
def __str__(self):
|
||||
return '{}/{}'.format(self.name, self.port)
|
||||
|
||||
@property
|
||||
def primary(self):
|
||||
primary_protocol_name = AllTypes.get_primary_protocol_name(
|
||||
self.platform.category, self.platform.type
|
||||
)
|
||||
return self.name == primary_protocol_name
|
||||
|
||||
@property
|
||||
def secret_types(self):
|
||||
return Protocol.settings().get(self.name, {}).get('secret_types')
|
||||
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
|
||||
|
||||
def set_public(self):
|
||||
private_protocol_set = ('winrm',)
|
||||
self.public = self.name not in private_protocol_set
|
||||
|
||||
def save(self, **kwargs):
|
||||
self.set_public()
|
||||
return super().save(**kwargs)
|
||||
|
||||
|
||||
class PlatformAutomation(models.Model):
|
||||
ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled"))
|
||||
ansible_config = models.JSONField(default=dict, verbose_name=_("Ansible config"))
|
||||
|
||||
ping_enabled = models.BooleanField(default=False, verbose_name=_("Ping enabled"))
|
||||
ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method"))
|
||||
ping_params = models.JSONField(default=dict, verbose_name=_("Ping params"))
|
||||
|
||||
gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
|
||||
gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method"))
|
||||
gather_facts_method = models.TextField(
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
|
||||
)
|
||||
gather_facts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
|
||||
|
||||
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled"))
|
||||
change_secret_method = models.TextField(
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Change secret method")
|
||||
)
|
||||
change_secret_params = models.JSONField(default=dict, verbose_name=_("Change secret params"))
|
||||
|
||||
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
|
||||
push_account_method = models.TextField(
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Push account method")
|
||||
)
|
||||
push_account_params = models.JSONField(default=dict, verbose_name=_("Push account params"))
|
||||
|
||||
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
|
||||
verify_account_method = models.TextField(
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Verify account method")
|
||||
)
|
||||
verify_account_params = models.JSONField(default=dict, verbose_name=_("Verify account params"))
|
||||
|
||||
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
|
||||
gather_accounts_method = models.TextField(
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
|
||||
)
|
||||
gather_accounts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
|
||||
|
||||
|
||||
class Platform(JMSBaseModel):
|
||||
|
@ -80,14 +92,18 @@ class Platform(JMSBaseModel):
|
|||
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
|
||||
# 资产有关的
|
||||
charset = models.CharField(
|
||||
default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
|
||||
default=CharsetChoices.utf8, choices=CharsetChoices.choices,
|
||||
max_length=8, verbose_name=_("Charset")
|
||||
)
|
||||
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
|
||||
# 账号有关的
|
||||
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
|
||||
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
|
||||
automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
|
||||
blank=True, null=True, verbose_name=_("Automation"))
|
||||
automation = models.OneToOneField(
|
||||
PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
|
||||
blank=True, null=True, verbose_name=_("Automation")
|
||||
)
|
||||
custom_fields = models.JSONField(null=True, default=list, verbose_name=_("Custom fields"))
|
||||
|
||||
@property
|
||||
def type_constraints(self):
|
||||
|
@ -100,11 +116,6 @@ class Platform(JMSBaseModel):
|
|||
)
|
||||
return linux.id
|
||||
|
||||
@property
|
||||
def primary_protocol(self):
|
||||
primary_protocol_name = AllTypes.get_primary_protocol_name(self.category, self.type)
|
||||
return self.protocols.filter(name=primary_protocol_name).first()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -1,6 +1,8 @@
|
|||
# No pass
|
||||
from .cloud import *
|
||||
from .common import *
|
||||
from .host import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .cloud import *
|
||||
from .host import *
|
||||
from .web import *
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from django.db.models import F
|
||||
from django.db.transaction import atomic
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.models import Account
|
||||
from accounts.serializers import AccountSerializerCreateValidateMixin
|
||||
from accounts.serializers import AuthValidateMixin
|
||||
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
|
||||
from accounts.serializers import AccountSerializer
|
||||
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \
|
||||
MethodSerializer
|
||||
from common.serializers.dynamic import create_serializer_class
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ...const import Category, AllTypes
|
||||
|
@ -19,9 +22,11 @@ __all__ = [
|
|||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
|
||||
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer',
|
||||
'AccountSecretSerializer', 'SpecSerializer'
|
||||
'AccountSecretSerializer', 'AssetProtocolsPermsSerializer'
|
||||
]
|
||||
|
||||
uuid_pattern = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
|
||||
|
||||
class AssetProtocolsSerializer(serializers.ModelSerializer):
|
||||
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1)
|
||||
|
@ -38,6 +43,11 @@ class AssetProtocolsSerializer(serializers.ModelSerializer):
|
|||
fields = ['name', 'port']
|
||||
|
||||
|
||||
class AssetProtocolsPermsSerializer(AssetProtocolsSerializer):
|
||||
class Meta(AssetProtocolsSerializer.Meta):
|
||||
fields = AssetProtocolsSerializer.Meta.fields + ['public', 'setting']
|
||||
|
||||
|
||||
class AssetLabelSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Label
|
||||
|
@ -59,45 +69,39 @@ class AssetPlatformSerializer(serializers.ModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class AssetAccountSerializer(
|
||||
AuthValidateMixin,
|
||||
AccountSerializerCreateValidateMixin,
|
||||
CommonModelSerializer
|
||||
):
|
||||
class AssetAccountSerializer(AccountSerializer):
|
||||
add_org_fields = False
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
template = serializers.BooleanField(
|
||||
default=False, label=_("Template"), write_only=True
|
||||
)
|
||||
name = serializers.CharField(max_length=128, required=True, label=_("Name"))
|
||||
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, required=False, write_only=True)
|
||||
clone_id: str
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields_mini = [
|
||||
'id', 'name', 'username', 'privileged',
|
||||
'is_active', 'version', 'secret_type',
|
||||
def to_internal_value(self, data):
|
||||
clone_id = data.pop('id', None)
|
||||
ret = super().to_internal_value(data)
|
||||
self.clone_id = clone_id
|
||||
return ret
|
||||
|
||||
def set_secret(self, attrs):
|
||||
_id = self.clone_id
|
||||
if not _id:
|
||||
return attrs
|
||||
|
||||
account = Account.objects.get(id=_id)
|
||||
attrs['secret'] = account.secret
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
return self.set_secret(attrs)
|
||||
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields = [
|
||||
f for f in AccountSerializer.Meta.fields
|
||||
if f not in ['spec_info']
|
||||
]
|
||||
fields_write_only = [
|
||||
'secret', 'passphrase', 'push_now', 'template'
|
||||
]
|
||||
fields = fields_mini + fields_write_only
|
||||
extra_kwargs = {
|
||||
'secret': {'write_only': True},
|
||||
**AccountSerializer.Meta.extra_kwargs,
|
||||
}
|
||||
|
||||
def validate_push_now(self, value):
|
||||
request = self.context['request']
|
||||
if not request.user.has_perms('accounts.push_account'):
|
||||
return False
|
||||
return value
|
||||
|
||||
def validate_name(self, value):
|
||||
if not value:
|
||||
value = self.initial_data.get('username')
|
||||
return value
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
||||
class Meta:
|
||||
|
@ -110,44 +114,32 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class SpecSerializer(serializers.Serializer):
|
||||
# 数据库
|
||||
db_name = serializers.CharField(label=_("Database"), max_length=128, required=False)
|
||||
use_ssl = serializers.BooleanField(label=_("Use SSL"), required=False)
|
||||
allow_invalid_cert = serializers.BooleanField(label=_("Allow invalid cert"), required=False)
|
||||
# Web
|
||||
autofill = serializers.CharField(label=_("Auto fill"), required=False)
|
||||
username_selector = serializers.CharField(label=_("Username selector"), required=False)
|
||||
password_selector = serializers.CharField(label=_("Password selector"), required=False)
|
||||
submit_selector = serializers.CharField(label=_("Submit selector"), required=False)
|
||||
script = serializers.JSONField(label=_("Script"), required=False)
|
||||
|
||||
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
||||
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
|
||||
nodes_display = serializers.ListField(read_only=True, label=_("Node path"))
|
||||
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
|
||||
custom_info = MethodSerializer(label=_('Custom info'))
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields_mini = ['id', 'name', 'address']
|
||||
fields_small = fields_mini + ['is_active', 'comment']
|
||||
fields_small = fields_mini + ['custom_info', 'is_active', 'comment']
|
||||
fields_fk = ['domain', 'platform']
|
||||
fields_m2m = [
|
||||
'nodes', 'labels', 'protocols',
|
||||
'nodes_display', 'accounts'
|
||||
'nodes_display', 'accounts',
|
||||
]
|
||||
read_only_fields = [
|
||||
'category', 'type', 'connectivity', 'auto_info',
|
||||
'category', 'type', 'connectivity', 'auto_config',
|
||||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
fields = fields_small + fields_fk + fields_m2m + read_only_fields
|
||||
fields_unexport = ['auto_info']
|
||||
fields_unexport = ['auto_config']
|
||||
extra_kwargs = {
|
||||
'auto_info': {'label': _('Auto info')},
|
||||
'auto_config': {'label': _('Auto info')},
|
||||
'name': {'label': _("Name")},
|
||||
'address': {'label': _('Address')},
|
||||
'nodes_display': {'label': _('Node path')},
|
||||
|
@ -197,6 +189,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
|||
.annotate(type=F("platform__type"))
|
||||
return queryset
|
||||
|
||||
def get_custom_info_serializer(self):
|
||||
request = self.context.get('request')
|
||||
default_field = serializers.DictField(required=False, label=_('Custom info'))
|
||||
|
||||
if not request:
|
||||
return default_field
|
||||
|
||||
if self.instance and isinstance(self.instance, list):
|
||||
return default_field
|
||||
|
||||
if not self.instance and uuid_pattern.findall(request.path):
|
||||
pk = uuid_pattern.findall(request.path)[0]
|
||||
self.instance = Asset.objects.filter(id=pk).first()
|
||||
|
||||
platform = None
|
||||
if self.instance:
|
||||
platform = self.instance.platform
|
||||
elif request.query_params.get('platform'):
|
||||
platform_id = request.query_params.get('platform')
|
||||
platform_id = int(platform_id) if platform_id.isdigit() else 0
|
||||
platform = Platform.objects.filter(id=platform_id).first()
|
||||
|
||||
if not platform:
|
||||
return default_field
|
||||
custom_fields = platform.custom_fields
|
||||
if not custom_fields:
|
||||
return default_field
|
||||
name = platform.name.title() + 'CustomSerializer'
|
||||
return create_serializer_class(name, custom_fields)()
|
||||
|
||||
@staticmethod
|
||||
def perform_nodes_display_create(instance, nodes_display):
|
||||
if not nodes_display:
|
||||
|
@ -276,8 +298,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
|||
if not accounts_data:
|
||||
return
|
||||
for data in accounts_data:
|
||||
data['asset'] = asset
|
||||
AssetAccountSerializer().create(data)
|
||||
data['asset'] = asset.id
|
||||
|
||||
s = AssetAccountSerializer(data=accounts_data, many=True)
|
||||
s.is_valid(raise_exception=True)
|
||||
s.save()
|
||||
|
||||
@atomic
|
||||
def create(self, validated_data):
|
||||
|
@ -300,16 +325,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
|||
|
||||
class DetailMixin(serializers.Serializer):
|
||||
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
|
||||
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||
auto_info = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||
spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
|
||||
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
|
||||
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||
|
||||
def get_instance(self):
|
||||
request = self.context.get('request')
|
||||
if not self.instance and uuid_pattern.findall(request.path):
|
||||
pk = uuid_pattern.findall(request.path)[0]
|
||||
self.instance = Asset.objects.filter(id=pk).first()
|
||||
return self.instance
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
names = super().get_field_names(declared_fields, info)
|
||||
names.extend([
|
||||
'accounts', 'info', 'spec_info', 'auto_info'
|
||||
'accounts', 'gathered_info', 'spec_info',
|
||||
'auto_config',
|
||||
])
|
||||
return names
|
||||
|
||||
def get_category(self):
|
||||
request = self.context.get('request')
|
||||
if request.query_params.get('category'):
|
||||
category = request.query_params.get('category')
|
||||
else:
|
||||
instance = self.get_instance()
|
||||
category = instance.category
|
||||
return category
|
||||
|
||||
def get_gathered_info_serializer(self):
|
||||
category = self.get_category()
|
||||
from .info.gathered import category_gathered_serializer_map
|
||||
serializer_cls = category_gathered_serializer_map.get(category, serializers.DictField)
|
||||
return serializer_cls()
|
||||
|
||||
def get_spec_info_serializer(self):
|
||||
category = self.get_category()
|
||||
from .info.spec import category_spec_serializer_map
|
||||
serializer_cls = category_spec_serializer_map.get(category, serializers.DictField)
|
||||
return serializer_cls()
|
||||
|
||||
|
||||
class AssetDetailSerializer(DetailMixin, AssetSerializer):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
from assets.models import Custom
|
||||
from .common import AssetSerializer
|
||||
|
||||
__all__ = ['CustomSerializer']
|
||||
|
||||
|
||||
class CustomSerializer(AssetSerializer):
|
||||
class Meta(AssetSerializer.Meta):
|
||||
model = Custom
|
|
@ -1,9 +1,9 @@
|
|||
from rest_framework.serializers import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from assets.models import Database
|
||||
from assets.serializers.gateway import GatewayWithAccountSecretSerializer
|
||||
from .common import AssetSerializer
|
||||
from ..gateway import GatewayWithAccountSecretSerializer
|
||||
|
||||
__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer']
|
||||
|
||||
|
|
|
@ -1,34 +1,18 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.models import Host
|
||||
from .common import AssetSerializer
|
||||
from .info.gathered import HostGatheredInfoSerializer
|
||||
|
||||
__all__ = ['HostInfoSerializer', 'HostSerializer']
|
||||
|
||||
|
||||
class HostInfoSerializer(serializers.Serializer):
|
||||
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
|
||||
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
|
||||
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
|
||||
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
|
||||
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
|
||||
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
|
||||
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
|
||||
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
|
||||
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
|
||||
|
||||
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
|
||||
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
|
||||
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
|
||||
__all__ = ['HostSerializer']
|
||||
|
||||
|
||||
class HostSerializer(AssetSerializer):
|
||||
info = HostInfoSerializer(required=False, label=_('Info'))
|
||||
gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info"))
|
||||
|
||||
class Meta(AssetSerializer.Meta):
|
||||
model = Host
|
||||
fields = AssetSerializer.Meta.fields + ['info']
|
||||
fields = AssetSerializer.Meta.fields + ['gathered_info']
|
||||
extra_kwargs = {
|
||||
**AssetSerializer.Meta.extra_kwargs,
|
||||
'address': {
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -24,6 +24,6 @@ class WebSerializer(AssetSerializer):
|
|||
'default': 'name=password'
|
||||
},
|
||||
'submit_selector': {
|
||||
'default': 'id=longin_button',
|
||||
'default': 'id=login_button',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from .asset import HostSerializer
|
||||
from .asset.common import AccountSecretSerializer
|
||||
from .asset.host import HostSerializer
|
||||
from ..models import Gateway, Asset
|
||||
|
||||
__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer']
|
||||
|
|
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
|||
from assets.const.web import FillType
|
||||
from common.serializers import WritableNestedModelSerializer
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.utils import lazyproperty
|
||||
from ..const import Category, AllTypes
|
||||
from ..models import Platform, PlatformProtocol, PlatformAutomation
|
||||
|
||||
|
@ -37,10 +38,12 @@ class ProtocolSettingSerializer(serializers.Serializer):
|
|||
default="", allow_blank=True, label=_("Submit selector")
|
||||
)
|
||||
script = serializers.JSONField(default=list, label=_("Script"))
|
||||
|
||||
# Redis
|
||||
auth_username = serializers.BooleanField(default=False, label=_("Auth with username"))
|
||||
|
||||
# WinRM
|
||||
use_ssl = serializers.BooleanField(default=False, label=_("Use SSL"))
|
||||
|
||||
|
||||
class PlatformAutomationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
@ -48,12 +51,12 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
|
|||
fields = [
|
||||
"id",
|
||||
"ansible_enabled", "ansible_config",
|
||||
"ping_enabled", "ping_method",
|
||||
"push_account_enabled", "push_account_method",
|
||||
"gather_facts_enabled", "gather_facts_method",
|
||||
"change_secret_enabled", "change_secret_method",
|
||||
"verify_account_enabled", "verify_account_method",
|
||||
"gather_accounts_enabled", "gather_accounts_method",
|
||||
"ping_enabled", "ping_method", "ping_params",
|
||||
"push_account_enabled", "push_account_method", "push_account_params",
|
||||
"gather_facts_enabled", "gather_facts_method", "gather_facts_params",
|
||||
"change_secret_enabled", "change_secret_method", "change_secret_params",
|
||||
"verify_account_enabled", "verify_account_method", "verify_account_params",
|
||||
"gather_accounts_enabled", "gather_accounts_method", "gather_accounts_params",
|
||||
]
|
||||
extra_kwargs = {
|
||||
# 启用资产探测
|
||||
|
@ -72,33 +75,53 @@ class PlatformAutomationSerializer(serializers.ModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class PlatformProtocolsSerializer(serializers.ModelSerializer):
|
||||
class PlatformProtocolSerializer(serializers.ModelSerializer):
|
||||
setting = ProtocolSettingSerializer(required=False, allow_null=True)
|
||||
primary = serializers.BooleanField(read_only=True, label=_("Primary"))
|
||||
|
||||
class Meta:
|
||||
model = PlatformProtocol
|
||||
fields = [
|
||||
"id", "name", "port", "primary",
|
||||
"default", "required", "secret_types",
|
||||
"setting",
|
||||
"required", "default",
|
||||
"secret_types", "setting",
|
||||
]
|
||||
|
||||
|
||||
class PlatformCustomField(serializers.Serializer):
|
||||
TYPE_CHOICES = [
|
||||
("str", "str"),
|
||||
("text", "text"),
|
||||
("int", "int"),
|
||||
("bool", "bool"),
|
||||
("choice", "choice"),
|
||||
("list", "list"),
|
||||
]
|
||||
name = serializers.CharField(label=_("Name"), max_length=128)
|
||||
label = serializers.CharField(label=_("Label"), max_length=128)
|
||||
type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str')
|
||||
default = serializers.CharField(default="", allow_blank=True, label=_("Default"), max_length=1024)
|
||||
help_text = serializers.CharField(default="", allow_blank=True, label=_("Help text"), max_length=1024)
|
||||
choices = serializers.ListField(default=list, label=_("Choices"), required=False)
|
||||
|
||||
|
||||
class PlatformSerializer(WritableNestedModelSerializer):
|
||||
charset = LabeledChoiceField(
|
||||
choices=Platform.CharsetChoices.choices, label=_("Charset")
|
||||
)
|
||||
SU_METHOD_CHOICES = [
|
||||
("sudo", "sudo su -"),
|
||||
("su", "su - "),
|
||||
("enable", "enable"),
|
||||
("super", "super 15"),
|
||||
("super_level", "super level 15")
|
||||
]
|
||||
charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
|
||||
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
|
||||
protocols = PlatformProtocolsSerializer(
|
||||
label=_("Protocols"), many=True, required=False
|
||||
)
|
||||
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
|
||||
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
|
||||
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
|
||||
su_method = LabeledChoiceField(
|
||||
choices=[("sudo", "sudo su -"), ("su", "su - ")],
|
||||
label=_("Su method"), required=False, default="sudo", allow_null=True
|
||||
choices=SU_METHOD_CHOICES, label=_("Su method"),
|
||||
required=False, default="sudo", allow_null=True
|
||||
)
|
||||
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
|
@ -106,19 +129,54 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
|||
fields_small = fields_mini + [
|
||||
"category", "type", "charset",
|
||||
]
|
||||
fields_other = [
|
||||
'date_created', 'date_updated', 'created_by', 'updated_by',
|
||||
read_only_fields = [
|
||||
'internal', 'date_created', 'date_updated',
|
||||
'created_by', 'updated_by'
|
||||
]
|
||||
fields = fields_small + [
|
||||
"protocols", "domain_enabled", "su_enabled",
|
||||
"su_method", "automation", "comment",
|
||||
] + fields_other
|
||||
"su_method", "automation", "comment", "custom_fields",
|
||||
] + read_only_fields
|
||||
extra_kwargs = {
|
||||
"su_enabled": {"label": _('Su enabled')},
|
||||
"domain_enabled": {"label": _('Domain enabled')},
|
||||
"domain_default": {"label": _('Default Domain')},
|
||||
}
|
||||
|
||||
@property
|
||||
def platform_category_type(self):
|
||||
if self.instance:
|
||||
return self.instance.category, self.instance.type
|
||||
if self.initial_data:
|
||||
return self.initial_data.get('category'), self.initial_data.get('type')
|
||||
raise serializers.ValidationError({'type': _("type is required")})
|
||||
|
||||
def add_type_choices(self, name, label):
|
||||
tp = self.fields['type']
|
||||
tp.choices[name] = label
|
||||
tp.choice_mapper[name] = label
|
||||
tp.choice_strings_to_values[name] = label
|
||||
|
||||
@lazyproperty
|
||||
def constraints(self):
|
||||
category, tp = self.platform_category_type
|
||||
constraints = AllTypes.get_constraints(category, tp)
|
||||
return constraints
|
||||
|
||||
def validate(self, attrs):
|
||||
domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False)
|
||||
su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False)
|
||||
automation = attrs.get('automation', {})
|
||||
automation['ansible_enabled'] = automation.get('ansible_enabled', False) \
|
||||
and self.constraints.get('ansible_enabled', False)
|
||||
attrs.update({
|
||||
'domain_enabled': domain_enabled,
|
||||
'su_enabled': su_enabled,
|
||||
'automation': automation,
|
||||
})
|
||||
self.initial_data['automation'] = automation
|
||||
return attrs
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.prefetch_related(
|
||||
|
@ -126,6 +184,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
|||
)
|
||||
return queryset
|
||||
|
||||
def validate_protocols(self, protocols):
|
||||
if not protocols:
|
||||
raise serializers.ValidationError(_("Protocols is required"))
|
||||
primary = [p for p in protocols if p.get('primary')]
|
||||
if not primary:
|
||||
protocols[0]['primary'] = True
|
||||
# 这里不设置不行,write_nested 不使用 validated 中的
|
||||
self.initial_data['protocols'] = protocols
|
||||
return protocols
|
||||
|
||||
|
||||
class PlatformOpsMethodSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(read_only=True)
|
||||
|
|
|
@ -66,11 +66,11 @@ def on_asset_create(sender, instance=None, created=False, **kwargs):
|
|||
ensure_asset_has_node(assets=(instance,))
|
||||
|
||||
# 获取资产硬件信息
|
||||
auto_info = instance.auto_info
|
||||
if auto_info.get('ping_enabled'):
|
||||
auto_config = instance.auto_config
|
||||
if auto_config.get('ping_enabled'):
|
||||
logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name))
|
||||
test_assets_connectivity_handler(assets=(instance,))
|
||||
if auto_info.get('gather_facts_enabled'):
|
||||
if auto_config.get('gather_facts_enabled'):
|
||||
logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name))
|
||||
gather_assets_facts_handler(assets=(instance,))
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device')
|
|||
router.register(r'databases', api.DatabaseViewSet, 'database')
|
||||
router.register(r'webs', api.WebViewSet, 'web')
|
||||
router.register(r'clouds', api.CloudViewSet, 'cloud')
|
||||
router.register(r'customs', api.CustomViewSet, 'custom')
|
||||
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
|
||||
router.register(r'labels', api.LabelViewSet, 'label')
|
||||
router.register(r'nodes', api.NodeViewSet, 'node')
|
||||
|
@ -45,6 +46,7 @@ urlpatterns = [
|
|||
path('nodes/<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('platform-automation-methods/', api.PlatformAutomationMethodsApi.as_view(), name='platform-automation-methods'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
@ -162,7 +162,8 @@ class RDPFileClientProtocolURLMixin:
|
|||
def get_smart_endpoint(self, protocol, asset=None):
|
||||
target_ip = asset.get_target_ip() if asset else ''
|
||||
endpoint = EndpointRule.match_endpoint(
|
||||
target_instance=asset, target_ip=target_ip, protocol=protocol, request=self.request
|
||||
target_instance=asset, target_ip=target_ip,
|
||||
protocol=protocol, request=self.request
|
||||
)
|
||||
return endpoint
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from assets.const import Protocol
|
||||
from assets.const.host import GATEWAY_NAME
|
||||
from common.db.fields import EncryptTextField
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import lazyproperty, pretty_string, bulk_get
|
||||
|
@ -231,12 +232,14 @@ class ConnectionToken(JMSOrgBaseModel):
|
|||
def domain(self):
|
||||
if not self.asset.platform.domain_enabled:
|
||||
return
|
||||
domain = self.asset.domain if self.asset else None
|
||||
if self.asset.platform.name == GATEWAY_NAME:
|
||||
return
|
||||
domain = self.asset.domain if self.asset.domain else None
|
||||
return domain
|
||||
|
||||
@lazyproperty
|
||||
def gateway(self):
|
||||
if not self.asset:
|
||||
if not self.asset or not self.domain:
|
||||
return
|
||||
return self.asset.gateway
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ from accounts.const import SecretType
|
|||
from accounts.models import Account
|
||||
from acls.models import CommandGroup, CommandFilterACL
|
||||
from assets.models import Asset, Platform, Gateway, Domain
|
||||
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
|
||||
from assets.serializers.asset import AssetProtocolsSerializer
|
||||
from assets.serializers.platform import PlatformSerializer
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
|
@ -26,18 +27,17 @@ class _ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
|||
|
||||
class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'))
|
||||
info = serializers.DictField()
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'id', 'name', 'address', 'protocols',
|
||||
'category', 'type', 'org_id', 'spec_info',
|
||||
'secret_info',
|
||||
'id', 'name', 'address', 'protocols', 'category',
|
||||
'type', 'org_id', 'info', 'secret_info', 'spec_info'
|
||||
]
|
||||
|
||||
|
||||
class _SimpleAccountSerializer(serializers.ModelSerializer):
|
||||
""" Account """
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
|
@ -46,20 +46,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
|
||||
""" Account """
|
||||
su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged'
|
||||
'id', 'name', 'username', 'secret_type',
|
||||
'secret', 'su_from', 'privileged'
|
||||
]
|
||||
|
||||
|
||||
class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||
""" Gateway """
|
||||
|
||||
account = _SimpleAccountSerializer(
|
||||
required=False, source='select_account', read_only=True
|
||||
)
|
||||
|
@ -85,7 +83,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = CommandFilterACL
|
||||
fields = [
|
||||
'id', 'name', 'command_groups', 'action', 'reviewers', 'priority', 'is_active'
|
||||
'id', 'name', 'command_groups', 'action',
|
||||
'reviewers', 'priority', 'is_active'
|
||||
]
|
||||
|
||||
|
||||
|
@ -136,8 +135,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
|||
'id', 'value', 'user', 'asset', 'account',
|
||||
'platform', 'command_filter_acls', 'protocol',
|
||||
'domain', 'gateway', 'actions', 'expire_at',
|
||||
'from_ticket',
|
||||
'expire_now', 'connect_method',
|
||||
'from_ticket', 'expire_now', 'connect_method',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'value': {'read_only': True},
|
||||
|
|
|
@ -111,7 +111,7 @@ class BaseFileParser(BaseParser):
|
|||
return {'pk': obj_id, 'name': obj_name}
|
||||
|
||||
def parse_value(self, field, value):
|
||||
if value is '-':
|
||||
if value == '-' and field and field.allow_null:
|
||||
return None
|
||||
elif hasattr(field, 'to_file_internal_value'):
|
||||
value = field.to_file_internal_value(value)
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .common import *
|
||||
from .dynamic import *
|
||||
from .mixin import *
|
||||
|
|
|
@ -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)
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import phonenumbers
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
@ -17,6 +18,7 @@ __all__ = [
|
|||
"BitChoicesField",
|
||||
"TreeChoicesField",
|
||||
"LabeledMultipleChoiceField",
|
||||
"PhoneField",
|
||||
]
|
||||
|
||||
|
||||
|
@ -201,3 +203,11 @@ class BitChoicesField(TreeChoicesField):
|
|||
value = self.to_internal_value(data)
|
||||
self.run_validators(value)
|
||||
return value
|
||||
|
||||
|
||||
class PhoneField(serializers.CharField):
|
||||
def to_representation(self, value):
|
||||
if value:
|
||||
phone = phonenumbers.parse(value, 'CN')
|
||||
value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number}
|
||||
return value
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
#
|
||||
import re
|
||||
|
||||
import phonenumbers
|
||||
|
||||
from django.core.validators import RegexValidator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.validators import (
|
||||
UniqueTogetherValidator, ValidationError
|
||||
)
|
||||
from rest_framework import serializers
|
||||
from phonenumbers.phonenumberutil import NumberParseException
|
||||
|
||||
from common.utils.strings import no_special_chars
|
||||
|
||||
|
@ -42,9 +45,14 @@ class NoSpecialChars:
|
|||
|
||||
|
||||
class PhoneValidator:
|
||||
pattern = re.compile(r"^1[3456789]\d{9}$")
|
||||
message = _('The mobile phone number format is incorrect')
|
||||
|
||||
def __call__(self, value):
|
||||
if not self.pattern.match(value):
|
||||
try:
|
||||
phone = phonenumbers.parse(value, 'CN')
|
||||
valid = phonenumbers.is_valid_number(phone)
|
||||
except NumberParseException:
|
||||
valid = False
|
||||
|
||||
if not valid:
|
||||
raise serializers.ValidationError(self.message)
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_noop
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework import permissions
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.exceptions import UserConfirmRequired
|
||||
from common.utils import i18n_fmt
|
||||
from audits.handler import create_or_update_operate_log
|
||||
from audits.const import ActionChoices
|
||||
from audits.const import ActionChoices, ActivityChoices
|
||||
from audits.models import ActivityLog
|
||||
|
||||
__all__ = [
|
||||
"PermissionsMixin",
|
||||
|
@ -49,38 +51,63 @@ class RecordViewLogMixin:
|
|||
ACTION = ActionChoices.view
|
||||
|
||||
@staticmethod
|
||||
def get_resource_display(request):
|
||||
def _filter_params(params):
|
||||
new_params = {}
|
||||
need_pop_params = ('format', 'order')
|
||||
for key, value in params.items():
|
||||
if key in need_pop_params:
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
value = list(filter(None, value))
|
||||
if value:
|
||||
new_params[key] = value
|
||||
return new_params
|
||||
|
||||
def get_resource_display(self, request):
|
||||
query_params = dict(request.query_params)
|
||||
if query_params.get("format"):
|
||||
query_params.pop("format")
|
||||
spm_filter = query_params.pop("spm") if query_params.get("spm") else None
|
||||
if not query_params and not spm_filter:
|
||||
display_message = _("Export all")
|
||||
params = self._filter_params(query_params)
|
||||
|
||||
spm_filter = params.pop("spm", None)
|
||||
|
||||
if not params and not spm_filter:
|
||||
display_message = gettext_noop("Export all")
|
||||
elif spm_filter:
|
||||
display_message = _("Export only selected items")
|
||||
display_message = gettext_noop("Export only selected items")
|
||||
else:
|
||||
query = ",".join(
|
||||
["%s=%s" % (key, value) for key, value in query_params.items()]
|
||||
["%s=%s" % (key, value) for key, value in params.items()]
|
||||
)
|
||||
display_message = _("Export filtered: %s") % query
|
||||
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
|
||||
return display_message
|
||||
|
||||
def record_logs(self, ids, **kwargs):
|
||||
resource_type = self.model._meta.verbose_name
|
||||
create_or_update_operate_log(
|
||||
self.ACTION, resource_type, force=True, **kwargs
|
||||
)
|
||||
detail = i18n_fmt(
|
||||
gettext_noop('User %s view/export secret'), self.request.user
|
||||
)
|
||||
activities = [
|
||||
ActivityLog(
|
||||
resource_id=getattr(resource_id, 'pk', resource_id),
|
||||
type=ActivityChoices.operate_log, detail=detail
|
||||
)
|
||||
for resource_id in ids
|
||||
]
|
||||
ActivityLog.objects.bulk_create(activities)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
response = super().list(request, *args, **kwargs)
|
||||
with translation.override('en'):
|
||||
resource_display = self.get_resource_display(request)
|
||||
resource_type = self.model._meta.verbose_name
|
||||
create_or_update_operate_log(
|
||||
self.ACTION, resource_type, force=True,
|
||||
resource_display=resource_display
|
||||
)
|
||||
ids = [q.id for q in self.get_queryset()]
|
||||
self.record_logs(ids, resource_display=resource_display)
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
with translation.override('en'):
|
||||
resource_type = self.model._meta.verbose_name
|
||||
create_or_update_operate_log(
|
||||
self.ACTION, resource_type, force=True, resource=self.get_object()
|
||||
)
|
||||
resource = self.get_object()
|
||||
self.record_logs([resource.id], resource=resource)
|
||||
return response
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6fa80b59b9b5f95a9cfcad8ec47eacd519bb962d139ab90463795a7b306a0a72
|
||||
size 137935
|
||||
oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
|
||||
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
Loading…
Reference in New Issue