perf: 优化支持 choices (#10151)

* perf: 支持自定义类型资产

* perf: 改名前

* perf: 优化支持 choices

* perf: 优化自定义资产

* perf: 优化资产的详情

* perf: 修改完成自定义平台和资产

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
pull/10166/head
fit2bot 2023-04-10 10:57:44 +08:00 committed by GitHub
parent cec176cc33
commit 1248458451
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1061 additions and 583 deletions

View File

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

View File

@ -158,7 +158,7 @@ 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):

View File

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

View File

@ -102,14 +102,13 @@ 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 = []
@ -128,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()
@ -163,6 +157,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
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
@ -182,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

View File

@ -0,0 +1,16 @@
from assets.models import Custom, Asset
from assets.serializers import CustomSerializer
from .asset import AssetViewSet
__all__ = ['CustomViewSet']
class CustomViewSet(AssetViewSet):
model = Custom
perm_model = Asset
def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = CustomSerializer
return serializer_classes

View File

@ -1,8 +1,5 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.models import Host, Asset
from assets.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)

View File

@ -23,7 +23,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
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):

View File

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

View File

@ -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']
@ -46,12 +47,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

View File

@ -4,6 +4,15 @@ 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,
@ -22,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, {})}
@ -37,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]['default'] = True
protocols = [
{'name': name, **settings.get(name, {})}
for name in choices
]
if protocols:
protocols[0]['default'] = True
return protocols
@classmethod
@ -58,21 +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

View File

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

View File

@ -0,0 +1,56 @@
from .base import BaseType
class CustomTypes(BaseType):
@classmethod
def get_choices(cls):
types = cls.get_custom_platforms().values_list('type', flat=True).distinct()
return [(t, t) for t in types]
@classmethod
def _get_base_constrains(cls) -> dict:
return {
'*': {
'charset_enabled': False,
'domain_enabled': False,
'su_enabled': False,
},
}
@classmethod
def _get_automation_constrains(cls) -> dict:
constrains = {
'*': {
'ansible_enabled': False,
'ansible_config': {},
'gather_facts_enabled': False,
'verify_account_enabled': False,
'change_secret_enabled': False,
'push_account_enabled': False,
'gather_accounts_enabled': False,
}
}
return constrains
@classmethod
def _get_protocol_constrains(cls) -> dict:
constrains = {}
for platform in cls.get_custom_platforms():
choices = list(platform.protocols.values_list('name', flat=True))
if platform.type in constrains:
choices = constrains[platform.type]['choices'] + choices
constrains[platform.type] = {'choices': choices}
return constrains
@classmethod
def internal_platforms(cls):
return {
# cls.PUBLIC: [],
# cls.PRIVATE: [{'name': 'Vmware-vSphere'}],
# cls.K8S: [{'name': 'Kubernetes'}],
}
@classmethod
def get_custom_platforms(cls):
from assets.models import Platform
return Platform.objects.filter(category='custom')

View File

@ -141,6 +141,6 @@ class Protocol(ChoicesMixin, models.TextChoices):
def protocol_secret_types(cls):
settings = cls.settings()
return {
protocol: settings[protocol]['secret_types']
protocol: settings[protocol]['secret_types'] or ['password']
for protocol in cls.settings()
}

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
from common.db.models import ChoicesMixin
from .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
@ -138,14 +146,20 @@ class AllTypes(ChoicesMixin):
(Category.DATABASE, DatabaseTypes),
(Category.CLOUD, CloudTypes),
(Category.WEB, WebTypes),
(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):

View File

@ -28,7 +28,6 @@ def migrate_internal_platforms(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20230315_1741'),
]
@ -39,6 +38,11 @@ class Migration(migrations.Migration):
name='primary',
field=models.BooleanField(default=False, verbose_name='Primary'),
),
migrations.AddField(
model_name='platformprotocol',
name='public',
field=models.BooleanField(default=True, verbose_name='Public'),
),
migrations.RunPython(migrate_platform_charset),
migrations.RunPython(migrate_platform_protocol_primary),
migrations.RunPython(migrate_internal_platforms),

View File

@ -0,0 +1,45 @@
# 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.RenameField(
model_name='asset',
old_name='info',
new_name='gathered_info',
),
migrations.AlterField(
model_name='asset',
name='gathered_info',
field=models.JSONField(blank=True, default=dict, verbose_name='Gathered info'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.17 on 2023-03-24 03:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0111_auto_20230321_1633'),
]
operations = [
migrations.AddField(
model_name='platformprotocol',
name='public',
field=models.BooleanField(default=True, verbose_name='Public'),
),
]

View File

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

View File

@ -108,7 +108,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 +149,26 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return self.get_spec_values(instance, spec_fields)
@lazyproperty
def auto_info(self):
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': False
}
if not automation:
return auto_config
auto_config.update({
'ping_enabled': automation.ping_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,
}
})
return auto_config
def get_target_ip(self):
return self.address

View File

@ -0,0 +1,8 @@
from django.utils.translation import gettext_lazy as _
from .common import Asset
class Custom(Asset):
class Meta:
verbose_name = _("Custom asset")

View File

@ -24,7 +24,7 @@ class PlatformProtocol(models.Model):
@property
def secret_types(self):
return Protocol.settings().get(self.name, {}).get('secret_types')
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
class PlatformAutomation(models.Model):
@ -69,14 +69,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):

View File

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

View File

@ -1,6 +1,8 @@
# -*- 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 _
@ -8,7 +10,9 @@ from rest_framework import serializers
from accounts.models import Account
from accounts.serializers import AccountSerializer
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \
MethodSerializer
from common.serializers.dynamic import create_serializer_class
from common.serializers.fields import LabeledChoiceField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes
@ -18,9 +22,11 @@ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer',
'AccountSecretSerializer', 'SpecSerializer'
'AccountSecretSerializer',
]
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)
@ -83,44 +89,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, label=_('Account'))
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
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')},
@ -170,6 +164,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,16 +300,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

View File

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

View File

@ -1,9 +1,9 @@
from rest_framework.serializers import ValidationError
from django.utils.translation import ugettext_lazy as _
from 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']

View File

@ -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': {

View File

@ -0,0 +1,23 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
class HostGatheredInfoSerializer(serializers.Serializer):
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
category_gathered_serializer_map = {
'host': HostGatheredInfoSerializer,
}

View File

@ -0,0 +1,24 @@
from rest_framework import serializers
from assets.models import Database, Web
class DatabaseSpecSerializer(serializers.ModelSerializer):
class Meta:
model = Database
fields = ['db_name', 'use_ssl', 'allow_invalid_cert']
class WebSpecSerializer(serializers.ModelSerializer):
class Meta:
model = Web
fields = [
'autofill', 'username_selector', 'password_selector',
'submit_selector', 'script'
]
category_spec_serializer_map = {
'database': DatabaseSpecSerializer,
'web': WebSpecSerializer,
}

View File

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

View File

@ -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,7 +38,6 @@ 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"))
@ -87,6 +87,21 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
]
class PlatformCustomField(serializers.Serializer):
TYPE_CHOICES = [
("str", "str"),
("int", "int"),
("bool", "bool"),
("choice", "choice"),
]
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):
SU_METHOD_CHOICES = [
("sudo", "sudo su -"),
@ -95,19 +110,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
("super", "super 15"),
("super_level", "super level 15")
]
charset = LabeledChoiceField(
choices=Platform.CharsetChoices.choices, label=_("Charset")
)
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 = PlatformProtocolSerializer(
label=_("Protocols"), many=True, required=False
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField(
choices=SU_METHOD_CHOICES, label=_("Su method"),
required=False, default="sudo", allow_null=True
)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
su_method = LabeledChoiceField(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
@ -115,19 +127,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(

View File

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

View File

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

View File

@ -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
@ -30,14 +31,12 @@ class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
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', 'spec_info', 'secret_info',
]
class _SimpleAccountSerializer(serializers.ModelSerializer):
""" Account """
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta:
@ -46,20 +45,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 +82,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 +134,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},

View File

@ -0,0 +1,54 @@
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,
}
def set_default_if_need(data, i):
field_name = data.pop('name', 'Attr{}'.format(i + 1))
data['name'] = field_name
if not data.get('label'):
data['label'] = field_name
return data
def set_default_by_type(tp, data, field_info):
if tp == 'str':
data['max_length'] = 4096
elif tp == 'choice':
choices = field_info.pop('choices', [])
if isinstance(choices, str):
choices = choices.split(',')
choices = [
(c, c.title()) if not isinstance(c, (tuple, list)) else c
for c in choices
]
data['choices'] = choices
return data
def create_serializer_class(serializer_name, fields_info):
serializer_fields = {}
fields_name = ['name', 'label', 'default', 'type', 'help_text']
for i, field_info in enumerate(fields_info):
data = {k: field_info.get(k) for k in fields_name}
field_type = data.pop('type', 'str')
data = set_default_by_type(field_type, data, field_info)
data = set_default_if_need(data, i)
field_name = data.pop('name')
field_class = type_field_map.get(field_type, serializers.CharField)
serializer_fields[field_name] = field_class(**data)
return type(serializer_name, (serializers.Serializer,), serializer_fields)

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:15e96f9f31e92077ac828e248a30678e53b7c867757ae6348ae9805bc64874bc
size 138124
oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
size 138303

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43695645a64669ba25c4fdfd413ce497a07592c320071b399cbb4f54466441e3
size 113361
oid sha256:035f9429613b541f229855a7d36c98e5f4736efce54dcd21119660dd6d89d94e
size 114269

File diff suppressed because it is too large Load Diff

View File

@ -64,13 +64,7 @@ class DownloadUploadMixin:
if instance and not update:
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
serializer = serializers.AppletSerializer(data=manifest, instance=instance)
serializer.is_valid(raise_exception=True)
save_to = default_storage.path('applets/{}'.format(name))
if os.path.exists(save_to):
shutil.rmtree(save_to)
shutil.move(tmp_dir, save_to)
serializer.save()
applet, serializer = Applet.install_from_dir(tmp_dir)
return Response(serializer.data, status=201)
@action(detail=True, methods=['get'])

View File

@ -12,7 +12,6 @@ from rest_framework.serializers import ValidationError
from common.db.models import JMSBaseModel
from common.utils import lazyproperty, get_logger
from jumpserver.utils import has_valid_xpack_license
logger = get_logger(__name__)
@ -91,24 +90,48 @@ class Applet(JMSBaseModel):
return manifest
@classmethod
def install_from_dir(cls, path):
def load_platform_if_need(cls, d):
from assets.serializers import PlatformSerializer
if not os.path.exists(os.path.join(d, 'platform.yml')):
return
try:
with open(os.path.join(d, 'platform.yml')) as f:
data = yaml.safe_load(f)
except Exception as e:
raise ValidationError({'error': _('Load platform.yml failed: {}').format(e)})
if data['category'] != 'custom':
raise ValidationError({'error': _('Only support custom platform')})
try:
tp = data['type']
except KeyError:
raise ValidationError({'error': _('Missing type in platform.yml')})
s = PlatformSerializer(data=data)
s.add_type_choices(tp, tp)
s.is_valid(raise_exception=True)
s.save()
@classmethod
def install_from_dir(cls, path, builtin=True):
from terminal.serializers import AppletSerializer
manifest = cls.validate_pkg(path)
name = manifest['name']
if not has_valid_xpack_license() and name.lower() in ('navicat',):
return
instance = cls.objects.filter(name=name).first()
serializer = AppletSerializer(instance=instance, data=manifest)
serializer.is_valid()
serializer.save(builtin=True)
pkg_path = default_storage.path('applets/{}'.format(name))
serializer.save(builtin=builtin)
cls.load_platform_if_need(path)
pkg_path = default_storage.path('applets/{}'.format(name))
if os.path.exists(pkg_path):
shutil.rmtree(pkg_path)
shutil.copytree(path, pkg_path)
return instance
return instance, serializer
def select_host_account(self):
# 选择激活的发布机

View File

@ -1,3 +1,5 @@
import time
import uuid
from django.conf import settings
from django.core.cache import cache
from django.db import models
@ -139,6 +141,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
if self.user:
setattr(self.user, SKIP_SIGNAL, True)
self.user.delete()
self.name = self.name + '_' + uuid.uuid4().hex[:8]
self.user = None
self.is_deleted = True
self.save()