mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-07-07 03:49:00 +00:00
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:
parent
cec176cc33
commit
1248458451
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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 *
|
||||||
|
@ -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
|
||||||
|
16
apps/assets/api/asset/custom.py
Normal file
16
apps/assets/api/asset/custom.py
Normal 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
|
@ -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)
|
|
||||||
|
@ -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):
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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,7 +46,11 @@ 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 = [
|
||||||
|
{'name': name, **settings.get(name, {})}
|
||||||
|
for name in choices
|
||||||
|
]
|
||||||
|
if protocols:
|
||||||
protocols[0]['default'] = True
|
protocols[0]['default'] = True
|
||||||
return protocols
|
return protocols
|
||||||
|
|
||||||
@ -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
|
|
||||||
]
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
56
apps/assets/const/custom.py
Normal file
56
apps/assets/const/custom.py
Normal 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')
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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):
|
||||||
|
@ -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),
|
||||||
|
45
apps/assets/migrations/0112_auto_20230404_1631.py
Normal file
45
apps/assets/migrations/0112_auto_20230404_1631.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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 *
|
|
||||||
|
@ -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
|
||||||
|
8
apps/assets/models/asset/custom.py
Normal file
8
apps/assets/models/asset/custom.py
Normal 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")
|
@ -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):
|
||||||
|
@ -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 *
|
||||||
|
@ -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
|
||||||
|
9
apps/assets/serializers/asset/custom.py
Normal file
9
apps/assets/serializers/asset/custom.py
Normal 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
|
@ -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']
|
||||||
|
|
||||||
|
@ -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': {
|
||||||
|
23
apps/assets/serializers/asset/info/gathered.py
Normal file
23
apps/assets/serializers/asset/info/gathered.py
Normal 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,
|
||||||
|
}
|
24
apps/assets/serializers/asset/info/spec.py
Normal file
24
apps/assets/serializers/asset/info/spec.py
Normal 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,
|
||||||
|
}
|
@ -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']
|
||||||
|
@ -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(
|
||||||
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
|
choices=SU_METHOD_CHOICES, label=_("Su method"),
|
||||||
su_method = LabeledChoiceField(choices=SU_METHOD_CHOICES,
|
required=False, default="sudo", allow_null=True
|
||||||
label=_("Su method"), required=False, default="sudo", allow_null=True
|
|
||||||
)
|
)
|
||||||
|
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
@ -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(
|
||||||
|
@ -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,))
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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},
|
||||||
|
54
apps/common/serializers/dynamic.py
Normal file
54
apps/common/serializers/dynamic.py
Normal 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)
|
@ -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
@ -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
@ -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'])
|
||||||
|
@ -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):
|
||||||
# 选择激活的发布机
|
# 选择激活的发布机
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user