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>
This commit is contained in:
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: else:
account = accounts[0] account = accounts[0]
asset = account.asset asset = account.asset
if not asset.auto_info['ansible_enabled'] or \ if not asset.auto_config['ansible_enabled'] or \
not asset.auto_info['ping_enabled']: not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError() raise NotSupportedTemporarilyError()
task = verify_accounts_connectivity_task.delay(account_ids) task = verify_accounts_connectivity_task.delay(account_ids)

View File

@ -158,7 +158,7 @@ class AccountAssetSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info'] fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
def to_internal_value(self, data): def to_internal_value(self, data):
if isinstance(data, dict): if isinstance(data, dict):

View File

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

View File

@ -102,14 +102,13 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
("platform", serializers.PlatformSerializer), ("platform", serializers.PlatformSerializer),
("suggestion", serializers.MiniAssetSerializer), ("suggestion", serializers.MiniAssetSerializer),
("gateways", serializers.GatewaySerializer), ("gateways", serializers.GatewaySerializer),
("spec_info", serializers.SpecSerializer),
) )
rbac_perms = ( rbac_perms = (
("match", "assets.match_asset"), ("match", "assets.match_asset"),
("platform", "assets.view_platform"), ("platform", "assets.view_platform"),
("gateways", "assets.view_gateway"), ("gateways", "assets.view_gateway"),
("spec_info", "assets.view_asset"), ("spec_info", "assets.view_asset"),
("info", "assets.view_asset"), ("gathered_info", "assets.view_asset"),
) )
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
skip_assets = [] skip_assets = []
@ -128,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
serializer = super().get_serializer(instance=asset.platform) serializer = super().get_serializer(instance=asset.platform)
return Response(serializer.data) return Response(serializer.data)
@action(methods=["GET"], detail=True, url_path="spec-info")
def spec_info(self, *args, **kwargs):
asset = super().get_object()
return Response(asset.spec_info)
@action(methods=["GET"], detail=True, url_path="gateways") @action(methods=["GET"], detail=True, url_path="gateways")
def gateways(self, *args, **kwargs): def gateways(self, *args, **kwargs):
asset = self.get_object() asset = self.get_object()
@ -163,6 +157,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
continue continue
self.skip_assets.append(asset) self.skip_assets.append(asset)
return bulk_data return bulk_data
def bulk_update(self, request, *args, **kwargs): def bulk_update(self, request, *args, **kwargs):
bulk_data = self.filter_bulk_update_data() bulk_data = self.filter_bulk_update_data()
request._full_data = bulk_data request._full_data = bulk_data
@ -182,8 +177,8 @@ class AssetsTaskMixin:
task = update_assets_hardware_info_manual(assets) task = update_assets_hardware_info_manual(assets)
else: else:
asset = assets[0] asset = assets[0]
if not asset.auto_info['ansible_enabled'] or \ if not asset.auto_config['ansible_enabled'] or \
not asset.auto_info['ping_enabled']: not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError() raise NotSupportedTemporarilyError()
task = test_assets_connectivity_manual(assets) task = test_assets_connectivity_manual(assets)
return task return task

View File

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

View File

@ -1,8 +1,5 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from assets.models import Host, Asset from assets.models import Host, Asset
from assets.serializers import HostSerializer, HostInfoSerializer from assets.serializers import HostSerializer
from .asset import AssetViewSet from .asset import AssetViewSet
__all__ = ['HostViewSet'] __all__ = ['HostViewSet']
@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet):
def get_serializer_classes(self): def get_serializer_classes(self):
serializer_classes = super().get_serializer_classes() serializer_classes = super().get_serializer_classes()
serializer_classes['default'] = HostSerializer serializer_classes['default'] = HostSerializer
serializer_classes['info'] = HostInfoSerializer
return serializer_classes return serializer_classes
@action(methods=["GET"], detail=True, url_path="info")
def info(self, *args, **kwargs):
asset = super().get_object()
serializer = self.get_serializer(asset.info)
data = serializer.data
data['asset'] = {
'id': asset.id, 'name': asset.name,
'address': asset.address
}
return Response(data)

View File

@ -23,7 +23,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.filter(type__in=AllTypes.get_types()) queryset = queryset.filter(type__in=AllTypes.get_types_values())
return queryset return queryset
def get_object(self): def get_object(self):

View File

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

View File

@ -1,8 +1,9 @@
import os
import yaml
import json import json
import os
from functools import partial from functools import partial
import yaml
def check_platform_method(manifest, manifest_path): def check_platform_method(manifest, manifest_path):
required_keys = ['category', 'method', 'name', 'id', 'type'] required_keys = ['category', 'method', 'name', 'id', 'type']
@ -46,12 +47,12 @@ def filter_key(manifest, attr, value):
return value in manifest_value or 'all' in manifest_value return value in manifest_value or 'all' in manifest_value
def filter_platform_methods(category, tp, method=None, methods=None): def filter_platform_methods(category, tp_name, method=None, methods=None):
methods = platform_automation_methods if methods is None else methods methods = platform_automation_methods if methods is None else methods
if category: if category:
methods = filter(partial(filter_key, attr='category', value=category), methods) methods = filter(partial(filter_key, attr='category', value=category), methods)
if tp: if tp_name:
methods = filter(partial(filter_key, attr='type', value=tp), methods) methods = filter(partial(filter_key, attr='type', value=tp_name), methods)
if method: if method:
methods = filter(lambda x: x['method'] == method, methods) methods = filter(lambda x: x['method'] == method, methods)
return methods return methods

View File

@ -4,6 +4,15 @@ from jumpserver.utils import has_valid_xpack_license
from .protocol import Protocol from .protocol import Protocol
class Type:
def __init__(self, label, value):
self.label = label
self.value = value
def __str__(self):
return self.value
class BaseType(TextChoices): class BaseType(TextChoices):
""" """
约束应该考虑代是对平台对限制避免多余对选项: mysql 开启 ssh, 约束应该考虑代是对平台对限制避免多余对选项: mysql 开启 ssh,
@ -22,7 +31,7 @@ class BaseType(TextChoices):
protocols_default = protocols.pop('*', {}) protocols_default = protocols.pop('*', {})
automation_default = automation.pop('*', {}) automation_default = automation.pop('*', {})
for k, v in cls.choices: for k, v in cls.get_choices():
tp_base = {**base_default, **base.get(k, {})} tp_base = {**base_default, **base.get(k, {})}
tp_auto = {**automation_default, **automation.get(k, {})} tp_auto = {**automation_default, **automation.get(k, {})}
tp_protocols = {**protocols_default, **protocols.get(k, {})} tp_protocols = {**protocols_default, **protocols.get(k, {})}
@ -37,8 +46,12 @@ class BaseType(TextChoices):
choices = protocol.get('choices', []) choices = protocol.get('choices', [])
if choices == '__self__': if choices == '__self__':
choices = [tp] choices = [tp]
protocols = [{'name': name, **settings.get(name, {})} for name in choices] protocols = [
protocols[0]['default'] = True {'name': name, **settings.get(name, {})}
for name in choices
]
if protocols:
protocols[0]['default'] = True
return protocols return protocols
@classmethod @classmethod
@ -58,21 +71,21 @@ class BaseType(TextChoices):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def get_community_types(cls): def _get_choices_to_types(cls):
raise NotImplementedError choices = cls.get_choices()
return [Type(label, value) for value, label in choices]
@classmethod @classmethod
def get_types(cls): def get_types(cls):
tps = [tp for tp in cls] tps = cls._get_choices_to_types()
if not has_valid_xpack_license(): if not has_valid_xpack_license():
tps = cls.get_community_types() tps = cls.get_community_types()
return tps return tps
@classmethod
def get_community_types(cls):
return cls._get_choices_to_types()
@classmethod @classmethod
def get_choices(cls): def get_choices(cls):
tps = cls.get_types() return cls.choices
cls_choices = cls.choices
return [
choice for choice in cls_choices
if choice[0] in tps
]

View File

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

View File

@ -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): def protocol_secret_types(cls):
settings = cls.settings() settings = cls.settings()
return { return {
protocol: settings[protocol]['secret_types'] protocol: settings[protocol]['secret_types'] or ['password']
for protocol in cls.settings() for protocol in cls.settings()
} }

View File

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

View File

@ -28,7 +28,6 @@ def migrate_internal_platforms(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('assets', '0110_auto_20230315_1741'), ('assets', '0110_auto_20230315_1741'),
] ]
@ -39,6 +38,11 @@ class Migration(migrations.Migration):
name='primary', name='primary',
field=models.BooleanField(default=False, verbose_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_charset),
migrations.RunPython(migrate_platform_protocol_primary), migrations.RunPython(migrate_platform_protocol_primary),
migrations.RunPython(migrate_internal_platforms), 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 .common import *
from .host import * from .custom import *
from .database import * from .database import *
from .device import * from .device import *
from .host import *
from .web import * from .web import *
from .cloud import *

View File

@ -108,7 +108,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
verbose_name=_("Nodes")) verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
info = models.JSONField(verbose_name=_('Info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息 gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
objects = AssetManager.from_queryset(AssetQuerySet)() objects = AssetManager.from_queryset(AssetQuerySet)()
@ -148,20 +149,26 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
return self.get_spec_values(instance, spec_fields) return self.get_spec_values(instance, spec_fields)
@lazyproperty @lazyproperty
def auto_info(self): def auto_config(self):
platform = self.platform platform = self.platform
automation = self.platform.automation automation = self.platform.automation
return { auto_config = {
'su_enabled': platform.su_enabled, 'su_enabled': platform.su_enabled,
'ping_enabled': automation.ping_enabled,
'domain_enabled': platform.domain_enabled, 'domain_enabled': platform.domain_enabled,
'ansible_enabled': False
}
if not automation:
return auto_config
auto_config.update({
'ping_enabled': automation.ping_enabled,
'ansible_enabled': automation.ansible_enabled, 'ansible_enabled': automation.ansible_enabled,
'push_account_enabled': automation.push_account_enabled, 'push_account_enabled': automation.push_account_enabled,
'gather_facts_enabled': automation.gather_facts_enabled, 'gather_facts_enabled': automation.gather_facts_enabled,
'change_secret_enabled': automation.change_secret_enabled, 'change_secret_enabled': automation.change_secret_enabled,
'verify_account_enabled': automation.verify_account_enabled, 'verify_account_enabled': automation.verify_account_enabled,
'gather_accounts_enabled': automation.gather_accounts_enabled, 'gather_accounts_enabled': automation.gather_accounts_enabled,
} })
return auto_config
def get_target_ip(self): def get_target_ip(self):
return self.address 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 @property
def secret_types(self): def secret_types(self):
return Protocol.settings().get(self.name, {}).get('secret_types') return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
class PlatformAutomation(models.Model): class PlatformAutomation(models.Model):
@ -69,14 +69,18 @@ class Platform(JMSBaseModel):
internal = models.BooleanField(default=False, verbose_name=_("Internal")) internal = models.BooleanField(default=False, verbose_name=_("Internal"))
# 资产有关的 # 资产有关的
charset = models.CharField( charset = models.CharField(
default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset") default=CharsetChoices.utf8, choices=CharsetChoices.choices,
max_length=8, verbose_name=_("Charset")
) )
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled")) domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
# 账号有关的 # 账号有关的
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled")) su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method")) su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform', automation = models.OneToOneField(
blank=True, null=True, verbose_name=_("Automation")) PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
blank=True, null=True, verbose_name=_("Automation")
)
custom_fields = models.JSONField(null=True, default=list, verbose_name=_("Custom fields"))
@property @property
def type_constraints(self): def type_constraints(self):

View File

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

View File

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import re
from django.db.models import F from django.db.models import F
from django.db.transaction import atomic from django.db.transaction import atomic
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -8,7 +10,9 @@ from rest_framework import serializers
from accounts.models import Account from accounts.models import Account
from accounts.serializers import AccountSerializer 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 common.serializers.fields import LabeledChoiceField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ...const import Category, AllTypes from ...const import Category, AllTypes
@ -18,9 +22,11 @@ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer', 'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer',
'AccountSecretSerializer', 'SpecSerializer' 'AccountSecretSerializer',
] ]
uuid_pattern = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
class AssetProtocolsSerializer(serializers.ModelSerializer): class AssetProtocolsSerializer(serializers.ModelSerializer):
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1) port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1)
@ -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): class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, 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")) nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
custom_info = MethodSerializer(label=_('Custom info'))
class Meta: class Meta:
model = Asset model = Asset
fields_mini = ['id', 'name', 'address'] fields_mini = ['id', 'name', 'address']
fields_small = fields_mini + ['is_active', 'comment'] fields_small = fields_mini + ['custom_info', 'is_active', 'comment']
fields_fk = ['domain', 'platform'] fields_fk = ['domain', 'platform']
fields_m2m = [ fields_m2m = [
'nodes', 'labels', 'protocols', 'nodes', 'labels', 'protocols',
'nodes_display', 'accounts' 'nodes_display', 'accounts',
] ]
read_only_fields = [ read_only_fields = [
'category', 'type', 'connectivity', 'auto_info', 'category', 'type', 'connectivity', 'auto_config',
'date_verified', 'created_by', 'date_created', 'date_verified', 'created_by', 'date_created',
] ]
fields = fields_small + fields_fk + fields_m2m + read_only_fields fields = fields_small + fields_fk + fields_m2m + read_only_fields
fields_unexport = ['auto_info'] fields_unexport = ['auto_config']
extra_kwargs = { extra_kwargs = {
'auto_info': {'label': _('Auto info')}, 'auto_config': {'label': _('Auto info')},
'name': {'label': _("Name")}, 'name': {'label': _("Name")},
'address': {'label': _('Address')}, 'address': {'label': _('Address')},
'nodes_display': {'label': _('Node path')}, 'nodes_display': {'label': _('Node path')},
@ -170,6 +164,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
.annotate(type=F("platform__type")) .annotate(type=F("platform__type"))
return queryset return queryset
def get_custom_info_serializer(self):
request = self.context.get('request')
default_field = serializers.DictField(required=False, label=_('Custom info'))
if not request:
return default_field
if self.instance and isinstance(self.instance, list):
return default_field
if not self.instance and uuid_pattern.findall(request.path):
pk = uuid_pattern.findall(request.path)[0]
self.instance = Asset.objects.filter(id=pk).first()
platform = None
if self.instance:
platform = self.instance.platform
elif request.query_params.get('platform'):
platform_id = request.query_params.get('platform')
platform_id = int(platform_id) if platform_id.isdigit() else 0
platform = Platform.objects.filter(id=platform_id).first()
if not platform:
return default_field
custom_fields = platform.custom_fields
if not custom_fields:
return default_field
name = platform.name.title() + 'CustomSerializer'
return create_serializer_class(name, custom_fields)()
@staticmethod @staticmethod
def perform_nodes_display_create(instance, nodes_display): def perform_nodes_display_create(instance, nodes_display):
if not nodes_display: if not nodes_display:
@ -276,16 +300,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
class DetailMixin(serializers.Serializer): class DetailMixin(serializers.Serializer):
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
spec_info = serializers.DictField(label=_('Spec info'), read_only=True) spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
auto_info = serializers.DictField(read_only=True, label=_('Auto info')) gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
def get_instance(self):
request = self.context.get('request')
if not self.instance and uuid_pattern.findall(request.path):
pk = uuid_pattern.findall(request.path)[0]
self.instance = Asset.objects.filter(id=pk).first()
return self.instance
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info) names = super().get_field_names(declared_fields, info)
names.extend([ names.extend([
'accounts', 'info', 'spec_info', 'auto_info' 'accounts', 'gathered_info', 'spec_info',
'auto_config',
]) ])
return names return names
def get_category(self):
request = self.context.get('request')
if request.query_params.get('category'):
category = request.query_params.get('category')
else:
instance = self.get_instance()
category = instance.category
return category
def get_gathered_info_serializer(self):
category = self.get_category()
from .info.gathered import category_gathered_serializer_map
serializer_cls = category_gathered_serializer_map.get(category, serializers.DictField)
return serializer_cls()
def get_spec_info_serializer(self):
category = self.get_category()
from .info.spec import category_spec_serializer_map
serializer_cls = category_spec_serializer_map.get(category, serializers.DictField)
return serializer_cls()
class AssetDetailSerializer(DetailMixin, AssetSerializer): class AssetDetailSerializer(DetailMixin, AssetSerializer):
pass pass

View File

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

View File

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

View File

@ -1,34 +1,18 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from assets.models import Host from assets.models import Host
from .common import AssetSerializer from .common import AssetSerializer
from .info.gathered import HostGatheredInfoSerializer
__all__ = ['HostInfoSerializer', 'HostSerializer'] __all__ = ['HostSerializer']
class HostInfoSerializer(serializers.Serializer):
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
class HostSerializer(AssetSerializer): class HostSerializer(AssetSerializer):
info = HostInfoSerializer(required=False, label=_('Info')) gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info"))
class Meta(AssetSerializer.Meta): class Meta(AssetSerializer.Meta):
model = Host model = Host
fields = AssetSerializer.Meta.fields + ['info'] fields = AssetSerializer.Meta.fields + ['gathered_info']
extra_kwargs = { extra_kwargs = {
**AssetSerializer.Meta.extra_kwargs, **AssetSerializer.Meta.extra_kwargs,
'address': { 'address': {

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from assets.const.web import FillType from assets.const.web import FillType
from common.serializers import WritableNestedModelSerializer from common.serializers import WritableNestedModelSerializer
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField
from common.utils import lazyproperty
from ..const import Category, AllTypes from ..const import Category, AllTypes
from ..models import Platform, PlatformProtocol, PlatformAutomation from ..models import Platform, PlatformProtocol, PlatformAutomation
@ -37,7 +38,6 @@ class ProtocolSettingSerializer(serializers.Serializer):
default="", allow_blank=True, label=_("Submit selector") default="", allow_blank=True, label=_("Submit selector")
) )
script = serializers.JSONField(default=list, label=_("Script")) script = serializers.JSONField(default=list, label=_("Script"))
# Redis # Redis
auth_username = serializers.BooleanField(default=False, label=_("Auth with username")) auth_username = serializers.BooleanField(default=False, label=_("Auth with username"))
@ -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): class PlatformSerializer(WritableNestedModelSerializer):
SU_METHOD_CHOICES = [ SU_METHOD_CHOICES = [
("sudo", "sudo su -"), ("sudo", "sudo su -"),
@ -95,19 +110,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
("super", "super 15"), ("super", "super 15"),
("super_level", "super level 15") ("super_level", "super level 15")
] ]
charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
charset = LabeledChoiceField(
choices=Platform.CharsetChoices.choices, label=_("Charset")
)
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type")) type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
category = LabeledChoiceField(choices=Category.choices, label=_("Category")) category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
protocols = PlatformProtocolSerializer( protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
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) custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
su_method = LabeledChoiceField(choices=SU_METHOD_CHOICES,
label=_("Su method"), required=False, default="sudo", allow_null=True
)
class Meta: class Meta:
model = Platform model = Platform
@ -115,19 +127,54 @@ class PlatformSerializer(WritableNestedModelSerializer):
fields_small = fields_mini + [ fields_small = fields_mini + [
"category", "type", "charset", "category", "type", "charset",
] ]
fields_other = [ read_only_fields = [
'date_created', 'date_updated', 'created_by', 'updated_by', 'internal', 'date_created', 'date_updated',
'created_by', 'updated_by'
] ]
fields = fields_small + [ fields = fields_small + [
"protocols", "domain_enabled", "su_enabled", "protocols", "domain_enabled", "su_enabled",
"su_method", "automation", "comment", "su_method", "automation", "comment", "custom_fields",
] + fields_other ] + read_only_fields
extra_kwargs = { extra_kwargs = {
"su_enabled": {"label": _('Su enabled')}, "su_enabled": {"label": _('Su enabled')},
"domain_enabled": {"label": _('Domain enabled')}, "domain_enabled": {"label": _('Domain enabled')},
"domain_default": {"label": _('Default Domain')}, "domain_default": {"label": _('Default Domain')},
} }
@property
def platform_category_type(self):
if self.instance:
return self.instance.category, self.instance.type
if self.initial_data:
return self.initial_data.get('category'), self.initial_data.get('type')
raise serializers.ValidationError({'type': _("type is required")})
def add_type_choices(self, name, label):
tp = self.fields['type']
tp.choices[name] = label
tp.choice_mapper[name] = label
tp.choice_strings_to_values[name] = label
@lazyproperty
def constraints(self):
category, tp = self.platform_category_type
constraints = AllTypes.get_constraints(category, tp)
return constraints
def validate(self, attrs):
domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False)
su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False)
automation = attrs.get('automation', {})
automation['ansible_enabled'] = automation.get('ansible_enabled', False) \
and self.constraints.get('ansible_enabled', False)
attrs.update({
'domain_enabled': domain_enabled,
'su_enabled': su_enabled,
'automation': automation,
})
self.initial_data['automation'] = automation
return attrs
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(

View File

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

View File

@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device')
router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'databases', api.DatabaseViewSet, 'database')
router.register(r'webs', api.WebViewSet, 'web') router.register(r'webs', api.WebViewSet, 'web')
router.register(r'clouds', api.CloudViewSet, 'cloud') router.register(r'clouds', api.CloudViewSet, 'cloud')
router.register(r'customs', api.CustomViewSet, 'custom')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'labels', api.LabelViewSet, 'label') router.register(r'labels', api.LabelViewSet, 'label')
router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'nodes', api.NodeViewSet, 'node')

View File

@ -5,7 +5,8 @@ from accounts.const import SecretType
from accounts.models import Account from accounts.models import Account
from acls.models import CommandGroup, CommandFilterACL from acls.models import CommandGroup, CommandFilterACL
from assets.models import Asset, Platform, Gateway, Domain from assets.models import Asset, Platform, Gateway, Domain
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer from assets.serializers.asset import AssetProtocolsSerializer
from assets.serializers.platform import PlatformSerializer
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField
from common.serializers.fields import ObjectRelatedField from common.serializers.fields import ObjectRelatedField
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
@ -30,14 +31,12 @@ class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
fields = [ fields = [
'id', 'name', 'address', 'protocols', 'id', 'name', 'address', 'protocols', 'category',
'category', 'type', 'org_id', 'spec_info', 'type', 'org_id', 'spec_info', 'secret_info',
'secret_info',
] ]
class _SimpleAccountSerializer(serializers.ModelSerializer): class _SimpleAccountSerializer(serializers.ModelSerializer):
""" Account """
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta: class Meta:
@ -46,20 +45,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
""" Account """
su_from = _SimpleAccountSerializer(required=False, label=_('Su from')) su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged' 'id', 'name', 'username', 'secret_type',
'secret', 'su_from', 'privileged'
] ]
class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer): class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
""" Gateway """
account = _SimpleAccountSerializer( account = _SimpleAccountSerializer(
required=False, source='select_account', read_only=True required=False, source='select_account', read_only=True
) )
@ -85,7 +82,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CommandFilterACL model = CommandFilterACL
fields = [ fields = [
'id', 'name', 'command_groups', 'action', 'reviewers', 'priority', 'is_active' 'id', 'name', 'command_groups', 'action',
'reviewers', 'priority', 'is_active'
] ]
@ -136,8 +134,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
'id', 'value', 'user', 'asset', 'account', 'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'protocol', 'platform', 'command_filter_acls', 'protocol',
'domain', 'gateway', 'actions', 'expire_at', 'domain', 'gateway', 'actions', 'expire_at',
'from_ticket', 'from_ticket', 'expire_now', 'connect_method',
'expire_now', 'connect_method',
] ]
extra_kwargs = { extra_kwargs = {
'value': {'read_only': True}, 'value': {'read_only': True},

View File

@ -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 version https://git-lfs.github.com/spec/v1
oid sha256:15e96f9f31e92077ac828e248a30678e53b7c867757ae6348ae9805bc64874bc oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
size 138124 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 version https://git-lfs.github.com/spec/v1
oid sha256:43695645a64669ba25c4fdfd413ce497a07592c320071b399cbb4f54466441e3 oid sha256:035f9429613b541f229855a7d36c98e5f4736efce54dcd21119660dd6d89d94e
size 113361 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: if instance and not update:
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400) return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
serializer = serializers.AppletSerializer(data=manifest, instance=instance) applet, serializer = Applet.install_from_dir(tmp_dir)
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()
return Response(serializer.data, status=201) return Response(serializer.data, status=201)
@action(detail=True, methods=['get']) @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.db.models import JMSBaseModel
from common.utils import lazyproperty, get_logger from common.utils import lazyproperty, get_logger
from jumpserver.utils import has_valid_xpack_license
logger = get_logger(__name__) logger = get_logger(__name__)
@ -91,24 +90,48 @@ class Applet(JMSBaseModel):
return manifest return manifest
@classmethod @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 from terminal.serializers import AppletSerializer
manifest = cls.validate_pkg(path) manifest = cls.validate_pkg(path)
name = manifest['name'] name = manifest['name']
if not has_valid_xpack_license() and name.lower() in ('navicat',):
return
instance = cls.objects.filter(name=name).first() instance = cls.objects.filter(name=name).first()
serializer = AppletSerializer(instance=instance, data=manifest) serializer = AppletSerializer(instance=instance, data=manifest)
serializer.is_valid() serializer.is_valid()
serializer.save(builtin=True) serializer.save(builtin=builtin)
pkg_path = default_storage.path('applets/{}'.format(name))
cls.load_platform_if_need(path)
pkg_path = default_storage.path('applets/{}'.format(name))
if os.path.exists(pkg_path): if os.path.exists(pkg_path):
shutil.rmtree(pkg_path) shutil.rmtree(pkg_path)
shutil.copytree(path, pkg_path) shutil.copytree(path, pkg_path)
return instance return instance, serializer
def select_host_account(self): def select_host_account(self):
# 选择激活的发布机 # 选择激活的发布机

View File

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