mirror of https://github.com/jumpserver/jumpserver
perf: 优化支持 choices (#10151)
* perf: 支持自定义类型资产 * perf: 改名前 * perf: 优化支持 choices * perf: 优化自定义资产 * perf: 优化资产的详情 * perf: 修改完成自定义平台和资产 --------- Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>pull/10166/head
parent
cec176cc33
commit
1248458451
|
@ -31,8 +31,8 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
|||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info']
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, dict):
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from .asset import *
|
||||
from .host import *
|
||||
from .database import *
|
||||
from .web import *
|
||||
from .cloud import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .host import *
|
||||
from .permission import *
|
||||
from .web import *
|
||||
|
|
|
@ -102,14 +102,13 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
("platform", serializers.PlatformSerializer),
|
||||
("suggestion", serializers.MiniAssetSerializer),
|
||||
("gateways", serializers.GatewaySerializer),
|
||||
("spec_info", serializers.SpecSerializer),
|
||||
)
|
||||
rbac_perms = (
|
||||
("match", "assets.match_asset"),
|
||||
("platform", "assets.view_platform"),
|
||||
("gateways", "assets.view_gateway"),
|
||||
("spec_info", "assets.view_asset"),
|
||||
("info", "assets.view_asset"),
|
||||
("gathered_info", "assets.view_asset"),
|
||||
)
|
||||
extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend]
|
||||
skip_assets = []
|
||||
|
@ -128,11 +127,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
serializer = super().get_serializer(instance=asset.platform)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=["GET"], detail=True, url_path="spec-info")
|
||||
def spec_info(self, *args, **kwargs):
|
||||
asset = super().get_object()
|
||||
return Response(asset.spec_info)
|
||||
|
||||
@action(methods=["GET"], detail=True, url_path="gateways")
|
||||
def gateways(self, *args, **kwargs):
|
||||
asset = self.get_object()
|
||||
|
@ -163,6 +157,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
|||
continue
|
||||
self.skip_assets.append(asset)
|
||||
return bulk_data
|
||||
|
||||
def bulk_update(self, request, *args, **kwargs):
|
||||
bulk_data = self.filter_bulk_update_data()
|
||||
request._full_data = bulk_data
|
||||
|
@ -182,8 +177,8 @@ class AssetsTaskMixin:
|
|||
task = update_assets_hardware_info_manual(assets)
|
||||
else:
|
||||
asset = assets[0]
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = test_assets_connectivity_manual(assets)
|
||||
return task
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
from assets.models import Custom, Asset
|
||||
from assets.serializers import CustomSerializer
|
||||
|
||||
from .asset import AssetViewSet
|
||||
|
||||
__all__ = ['CustomViewSet']
|
||||
|
||||
|
||||
class CustomViewSet(AssetViewSet):
|
||||
model = Custom
|
||||
perm_model = Asset
|
||||
|
||||
def get_serializer_classes(self):
|
||||
serializer_classes = super().get_serializer_classes()
|
||||
serializer_classes['default'] = CustomSerializer
|
||||
return serializer_classes
|
|
@ -1,8 +1,5 @@
|
|||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from assets.models import Host, Asset
|
||||
from assets.serializers import HostSerializer, HostInfoSerializer
|
||||
from assets.serializers import HostSerializer
|
||||
from .asset import AssetViewSet
|
||||
|
||||
__all__ = ['HostViewSet']
|
||||
|
@ -15,16 +12,4 @@ class HostViewSet(AssetViewSet):
|
|||
def get_serializer_classes(self):
|
||||
serializer_classes = super().get_serializer_classes()
|
||||
serializer_classes['default'] = HostSerializer
|
||||
serializer_classes['info'] = HostInfoSerializer
|
||||
return serializer_classes
|
||||
|
||||
@action(methods=["GET"], detail=True, url_path="info")
|
||||
def info(self, *args, **kwargs):
|
||||
asset = super().get_object()
|
||||
serializer = self.get_serializer(asset.info)
|
||||
data = serializer.data
|
||||
data['asset'] = {
|
||||
'id': asset.id, 'name': asset.name,
|
||||
'address': asset.address
|
||||
}
|
||||
return Response(data)
|
||||
|
|
|
@ -23,7 +23,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
|
|||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type__in=AllTypes.get_types())
|
||||
queryset = queryset.filter(type__in=AllTypes.get_types_values())
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
|
|
|
@ -29,7 +29,7 @@ class GatherFactsManager(BasePlaybookManager):
|
|||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
info = self.format_asset_info(asset.type, info)
|
||||
asset.info = info
|
||||
asset.save(update_fields=['info'])
|
||||
asset.gathered_info = info
|
||||
asset.save(update_fields=['gathered_info'])
|
||||
else:
|
||||
logger.error("Not found info: {}".format(host))
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import yaml
|
||||
import json
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def check_platform_method(manifest, manifest_path):
|
||||
required_keys = ['category', 'method', 'name', 'id', 'type']
|
||||
|
@ -46,12 +47,12 @@ def filter_key(manifest, attr, value):
|
|||
return value in manifest_value or 'all' in manifest_value
|
||||
|
||||
|
||||
def filter_platform_methods(category, tp, method=None, methods=None):
|
||||
def filter_platform_methods(category, tp_name, method=None, methods=None):
|
||||
methods = platform_automation_methods if methods is None else methods
|
||||
if category:
|
||||
methods = filter(partial(filter_key, attr='category', value=category), methods)
|
||||
if tp:
|
||||
methods = filter(partial(filter_key, attr='type', value=tp), methods)
|
||||
if tp_name:
|
||||
methods = filter(partial(filter_key, attr='type', value=tp_name), methods)
|
||||
if method:
|
||||
methods = filter(lambda x: x['method'] == method, methods)
|
||||
return methods
|
||||
|
|
|
@ -4,6 +4,15 @@ from jumpserver.utils import has_valid_xpack_license
|
|||
from .protocol import Protocol
|
||||
|
||||
|
||||
class Type:
|
||||
def __init__(self, label, value):
|
||||
self.label = label
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class BaseType(TextChoices):
|
||||
"""
|
||||
约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh,
|
||||
|
@ -22,7 +31,7 @@ class BaseType(TextChoices):
|
|||
protocols_default = protocols.pop('*', {})
|
||||
automation_default = automation.pop('*', {})
|
||||
|
||||
for k, v in cls.choices:
|
||||
for k, v in cls.get_choices():
|
||||
tp_base = {**base_default, **base.get(k, {})}
|
||||
tp_auto = {**automation_default, **automation.get(k, {})}
|
||||
tp_protocols = {**protocols_default, **protocols.get(k, {})}
|
||||
|
@ -37,8 +46,12 @@ class BaseType(TextChoices):
|
|||
choices = protocol.get('choices', [])
|
||||
if choices == '__self__':
|
||||
choices = [tp]
|
||||
protocols = [{'name': name, **settings.get(name, {})} for name in choices]
|
||||
protocols[0]['default'] = True
|
||||
protocols = [
|
||||
{'name': name, **settings.get(name, {})}
|
||||
for name in choices
|
||||
]
|
||||
if protocols:
|
||||
protocols[0]['default'] = True
|
||||
return protocols
|
||||
|
||||
@classmethod
|
||||
|
@ -58,21 +71,21 @@ class BaseType(TextChoices):
|
|||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_community_types(cls):
|
||||
raise NotImplementedError
|
||||
def _get_choices_to_types(cls):
|
||||
choices = cls.get_choices()
|
||||
return [Type(label, value) for value, label in choices]
|
||||
|
||||
@classmethod
|
||||
def get_types(cls):
|
||||
tps = [tp for tp in cls]
|
||||
tps = cls._get_choices_to_types()
|
||||
if not has_valid_xpack_license():
|
||||
tps = cls.get_community_types()
|
||||
return tps
|
||||
|
||||
@classmethod
|
||||
def get_community_types(cls):
|
||||
return cls._get_choices_to_types()
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
tps = cls.get_types()
|
||||
cls_choices = cls.choices
|
||||
return [
|
||||
choice for choice in cls_choices
|
||||
if choice[0] in tps
|
||||
]
|
||||
return cls.choices
|
||||
|
|
|
@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from common.db.models import ChoicesMixin
|
||||
|
||||
|
||||
__all__ = ['Category']
|
||||
|
||||
|
||||
|
@ -13,13 +12,10 @@ class Category(ChoicesMixin, models.TextChoices):
|
|||
DATABASE = 'database', _("Database")
|
||||
CLOUD = 'cloud', _("Cloud service")
|
||||
WEB = 'web', _("Web")
|
||||
CUSTOM = 'custom', _("Custom type")
|
||||
|
||||
@classmethod
|
||||
def filter_choices(cls, category):
|
||||
_category = getattr(cls, category.upper(), None)
|
||||
choices = [(_category.value, _category.label)] if _category else cls.choices
|
||||
return choices
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
settings = cls.settings()
|
||||
return {
|
||||
protocol: settings[protocol]['secret_types']
|
||||
protocol: settings[protocol]['secret_types'] or ['password']
|
||||
for protocol in cls.settings()
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext as _
|
|||
from common.db.models import ChoicesMixin
|
||||
from .category import Category
|
||||
from .cloud import CloudTypes
|
||||
from .custom import CustomTypes
|
||||
from .database import DatabaseTypes
|
||||
from .device import DeviceTypes
|
||||
from .host import HostTypes
|
||||
|
@ -16,7 +17,7 @@ class AllTypes(ChoicesMixin):
|
|||
choices: list
|
||||
includes = [
|
||||
HostTypes, DeviceTypes, DatabaseTypes,
|
||||
CloudTypes, WebTypes,
|
||||
CloudTypes, WebTypes, CustomTypes
|
||||
]
|
||||
_category_constrains = {}
|
||||
|
||||
|
@ -24,22 +25,29 @@ class AllTypes(ChoicesMixin):
|
|||
def choices(cls):
|
||||
choices = []
|
||||
for tp in cls.includes:
|
||||
choices.extend(tp.choices)
|
||||
choices.extend(tp.get_choices())
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
return cls.choices()
|
||||
|
||||
@classmethod
|
||||
def filter_choices(cls, category):
|
||||
choices = dict(cls.category_types()).get(category, cls).choices
|
||||
choices = dict(cls.category_types()).get(category, cls).get_choices()
|
||||
return choices() if callable(choices) else choices
|
||||
|
||||
@classmethod
|
||||
def get_constraints(cls, category, tp):
|
||||
def get_constraints(cls, category, tp_name):
|
||||
if not isinstance(tp_name, str):
|
||||
tp_name = tp_name.value
|
||||
|
||||
types_cls = dict(cls.category_types()).get(category)
|
||||
if not types_cls:
|
||||
return {}
|
||||
type_constraints = types_cls.get_constrains()
|
||||
constraints = type_constraints.get(tp, {})
|
||||
cls.set_automation_methods(category, tp, constraints)
|
||||
constraints = type_constraints.get(tp_name, {})
|
||||
cls.set_automation_methods(category, tp_name, constraints)
|
||||
return constraints
|
||||
|
||||
@classmethod
|
||||
|
@ -56,7 +64,7 @@ class AllTypes(ChoicesMixin):
|
|||
return asset_methods + account_methods
|
||||
|
||||
@classmethod
|
||||
def set_automation_methods(cls, category, tp, constraints):
|
||||
def set_automation_methods(cls, category, tp_name, constraints):
|
||||
from assets.automations import filter_platform_methods
|
||||
automation = constraints.get('automation', {})
|
||||
automation_methods = {}
|
||||
|
@ -66,7 +74,7 @@ class AllTypes(ChoicesMixin):
|
|||
continue
|
||||
item_name = item.replace('_enabled', '')
|
||||
methods = filter_platform_methods(
|
||||
category, tp, item_name, methods=platform_automation_methods
|
||||
category, tp_name, item_name, methods=platform_automation_methods
|
||||
)
|
||||
methods = [{'name': m['name'], 'id': m['id']} for m in methods]
|
||||
automation_methods[item_name + '_methods'] = methods
|
||||
|
@ -113,7 +121,7 @@ class AllTypes(ChoicesMixin):
|
|||
|
||||
@classmethod
|
||||
def grouped_choices(cls):
|
||||
grouped_types = [(str(ca), tp.choices) for ca, tp in cls.category_types()]
|
||||
grouped_types = [(str(ca), tp.get_choices()) for ca, tp in cls.category_types()]
|
||||
return grouped_types
|
||||
|
||||
@classmethod
|
||||
|
@ -138,14 +146,20 @@ class AllTypes(ChoicesMixin):
|
|||
(Category.DATABASE, DatabaseTypes),
|
||||
(Category.CLOUD, CloudTypes),
|
||||
(Category.WEB, WebTypes),
|
||||
(Category.CUSTOM, CustomTypes),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_types(cls):
|
||||
tps = []
|
||||
choices = []
|
||||
for i in dict(cls.category_types()).values():
|
||||
tps.extend(i.get_types())
|
||||
return tps
|
||||
choices.extend(i.get_types())
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_types_values(cls):
|
||||
choices = cls.get_types()
|
||||
return [c.value for c in choices]
|
||||
|
||||
@staticmethod
|
||||
def choice_to_node(choice, pid, opened=True, is_parent=True, meta=None):
|
||||
|
|
|
@ -28,7 +28,6 @@ def migrate_internal_platforms(apps, schema_editor):
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0110_auto_20230315_1741'),
|
||||
]
|
||||
|
@ -39,6 +38,11 @@ class Migration(migrations.Migration):
|
|||
name='primary',
|
||||
field=models.BooleanField(default=False, verbose_name='Primary'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platformprotocol',
|
||||
name='public',
|
||||
field=models.BooleanField(default=True, verbose_name='Public'),
|
||||
),
|
||||
migrations.RunPython(migrate_platform_charset),
|
||||
migrations.RunPython(migrate_platform_protocol_primary),
|
||||
migrations.RunPython(migrate_internal_platforms),
|
||||
|
|
|
@ -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 .host import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .host import *
|
||||
from .web import *
|
||||
from .cloud import *
|
||||
|
|
|
@ -108,7 +108,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
|||
verbose_name=_("Nodes"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
info = models.JSONField(verbose_name=_('Info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
|
||||
|
||||
objects = AssetManager.from_queryset(AssetQuerySet)()
|
||||
|
||||
|
@ -148,20 +149,26 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel):
|
|||
return self.get_spec_values(instance, spec_fields)
|
||||
|
||||
@lazyproperty
|
||||
def auto_info(self):
|
||||
def auto_config(self):
|
||||
platform = self.platform
|
||||
automation = self.platform.automation
|
||||
return {
|
||||
auto_config = {
|
||||
'su_enabled': platform.su_enabled,
|
||||
'ping_enabled': automation.ping_enabled,
|
||||
'domain_enabled': platform.domain_enabled,
|
||||
'ansible_enabled': False
|
||||
}
|
||||
if not automation:
|
||||
return auto_config
|
||||
auto_config.update({
|
||||
'ping_enabled': automation.ping_enabled,
|
||||
'ansible_enabled': automation.ansible_enabled,
|
||||
'push_account_enabled': automation.push_account_enabled,
|
||||
'gather_facts_enabled': automation.gather_facts_enabled,
|
||||
'change_secret_enabled': automation.change_secret_enabled,
|
||||
'verify_account_enabled': automation.verify_account_enabled,
|
||||
'gather_accounts_enabled': automation.gather_accounts_enabled,
|
||||
}
|
||||
})
|
||||
return auto_config
|
||||
|
||||
def get_target_ip(self):
|
||||
return self.address
|
||||
|
|
|
@ -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
|
||||
def secret_types(self):
|
||||
return Protocol.settings().get(self.name, {}).get('secret_types')
|
||||
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
|
||||
|
||||
|
||||
class PlatformAutomation(models.Model):
|
||||
|
@ -69,14 +69,18 @@ class Platform(JMSBaseModel):
|
|||
internal = models.BooleanField(default=False, verbose_name=_("Internal"))
|
||||
# 资产有关的
|
||||
charset = models.CharField(
|
||||
default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
|
||||
default=CharsetChoices.utf8, choices=CharsetChoices.choices,
|
||||
max_length=8, verbose_name=_("Charset")
|
||||
)
|
||||
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
|
||||
# 账号有关的
|
||||
su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled"))
|
||||
su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method"))
|
||||
automation = models.OneToOneField(PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
|
||||
blank=True, null=True, verbose_name=_("Automation"))
|
||||
automation = models.OneToOneField(
|
||||
PlatformAutomation, on_delete=models.CASCADE, related_name='platform',
|
||||
blank=True, null=True, verbose_name=_("Automation")
|
||||
)
|
||||
custom_fields = models.JSONField(null=True, default=list, verbose_name=_("Custom fields"))
|
||||
|
||||
@property
|
||||
def type_constraints(self):
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# No pass
|
||||
from .cloud import *
|
||||
from .common import *
|
||||
from .host import *
|
||||
from .custom import *
|
||||
from .database import *
|
||||
from .device import *
|
||||
from .cloud import *
|
||||
from .host import *
|
||||
from .web import *
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from django.db.models import F
|
||||
from django.db.transaction import atomic
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -8,7 +10,9 @@ from rest_framework import serializers
|
|||
|
||||
from accounts.models import Account
|
||||
from accounts.serializers import AccountSerializer
|
||||
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer
|
||||
from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer, \
|
||||
MethodSerializer
|
||||
from common.serializers.dynamic import create_serializer_class
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ...const import Category, AllTypes
|
||||
|
@ -18,9 +22,11 @@ __all__ = [
|
|||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer',
|
||||
'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer',
|
||||
'AccountSecretSerializer', 'SpecSerializer'
|
||||
'AccountSecretSerializer',
|
||||
]
|
||||
|
||||
uuid_pattern = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
|
||||
|
||||
class AssetProtocolsSerializer(serializers.ModelSerializer):
|
||||
port = serializers.IntegerField(required=False, allow_null=True, max_value=65535, min_value=1)
|
||||
|
@ -83,44 +89,32 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class SpecSerializer(serializers.Serializer):
|
||||
# 数据库
|
||||
db_name = serializers.CharField(label=_("Database"), max_length=128, required=False)
|
||||
use_ssl = serializers.BooleanField(label=_("Use SSL"), required=False)
|
||||
allow_invalid_cert = serializers.BooleanField(label=_("Allow invalid cert"), required=False)
|
||||
# Web
|
||||
autofill = serializers.CharField(label=_("Auto fill"), required=False)
|
||||
username_selector = serializers.CharField(label=_("Username selector"), required=False)
|
||||
password_selector = serializers.CharField(label=_("Password selector"), required=False)
|
||||
submit_selector = serializers.CharField(label=_("Submit selector"), required=False)
|
||||
script = serializers.JSONField(label=_("Script"), required=False)
|
||||
|
||||
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
||||
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, label=_('Account'))
|
||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
|
||||
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
|
||||
custom_info = MethodSerializer(label=_('Custom info'))
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields_mini = ['id', 'name', 'address']
|
||||
fields_small = fields_mini + ['is_active', 'comment']
|
||||
fields_small = fields_mini + ['custom_info', 'is_active', 'comment']
|
||||
fields_fk = ['domain', 'platform']
|
||||
fields_m2m = [
|
||||
'nodes', 'labels', 'protocols',
|
||||
'nodes_display', 'accounts'
|
||||
'nodes_display', 'accounts',
|
||||
]
|
||||
read_only_fields = [
|
||||
'category', 'type', 'connectivity', 'auto_info',
|
||||
'category', 'type', 'connectivity', 'auto_config',
|
||||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
fields = fields_small + fields_fk + fields_m2m + read_only_fields
|
||||
fields_unexport = ['auto_info']
|
||||
fields_unexport = ['auto_config']
|
||||
extra_kwargs = {
|
||||
'auto_info': {'label': _('Auto info')},
|
||||
'auto_config': {'label': _('Auto info')},
|
||||
'name': {'label': _("Name")},
|
||||
'address': {'label': _('Address')},
|
||||
'nodes_display': {'label': _('Node path')},
|
||||
|
@ -170,6 +164,36 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
|||
.annotate(type=F("platform__type"))
|
||||
return queryset
|
||||
|
||||
def get_custom_info_serializer(self):
|
||||
request = self.context.get('request')
|
||||
default_field = serializers.DictField(required=False, label=_('Custom info'))
|
||||
|
||||
if not request:
|
||||
return default_field
|
||||
|
||||
if self.instance and isinstance(self.instance, list):
|
||||
return default_field
|
||||
|
||||
if not self.instance and uuid_pattern.findall(request.path):
|
||||
pk = uuid_pattern.findall(request.path)[0]
|
||||
self.instance = Asset.objects.filter(id=pk).first()
|
||||
|
||||
platform = None
|
||||
if self.instance:
|
||||
platform = self.instance.platform
|
||||
elif request.query_params.get('platform'):
|
||||
platform_id = request.query_params.get('platform')
|
||||
platform_id = int(platform_id) if platform_id.isdigit() else 0
|
||||
platform = Platform.objects.filter(id=platform_id).first()
|
||||
|
||||
if not platform:
|
||||
return default_field
|
||||
custom_fields = platform.custom_fields
|
||||
if not custom_fields:
|
||||
return default_field
|
||||
name = platform.name.title() + 'CustomSerializer'
|
||||
return create_serializer_class(name, custom_fields)()
|
||||
|
||||
@staticmethod
|
||||
def perform_nodes_display_create(instance, nodes_display):
|
||||
if not nodes_display:
|
||||
|
@ -276,16 +300,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
|||
|
||||
class DetailMixin(serializers.Serializer):
|
||||
accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts'))
|
||||
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||
auto_info = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||
spec_info = MethodSerializer(label=_('Spec info'), read_only=True)
|
||||
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
|
||||
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
|
||||
|
||||
def get_instance(self):
|
||||
request = self.context.get('request')
|
||||
if not self.instance and uuid_pattern.findall(request.path):
|
||||
pk = uuid_pattern.findall(request.path)[0]
|
||||
self.instance = Asset.objects.filter(id=pk).first()
|
||||
return self.instance
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
names = super().get_field_names(declared_fields, info)
|
||||
names.extend([
|
||||
'accounts', 'info', 'spec_info', 'auto_info'
|
||||
'accounts', 'gathered_info', 'spec_info',
|
||||
'auto_config',
|
||||
])
|
||||
return names
|
||||
|
||||
def get_category(self):
|
||||
request = self.context.get('request')
|
||||
if request.query_params.get('category'):
|
||||
category = request.query_params.get('category')
|
||||
else:
|
||||
instance = self.get_instance()
|
||||
category = instance.category
|
||||
return category
|
||||
|
||||
def get_gathered_info_serializer(self):
|
||||
category = self.get_category()
|
||||
from .info.gathered import category_gathered_serializer_map
|
||||
serializer_cls = category_gathered_serializer_map.get(category, serializers.DictField)
|
||||
return serializer_cls()
|
||||
|
||||
def get_spec_info_serializer(self):
|
||||
category = self.get_category()
|
||||
from .info.spec import category_spec_serializer_map
|
||||
serializer_cls = category_spec_serializer_map.get(category, serializers.DictField)
|
||||
return serializer_cls()
|
||||
|
||||
|
||||
class AssetDetailSerializer(DetailMixin, AssetSerializer):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
from assets.models import Custom
|
||||
from .common import AssetSerializer
|
||||
|
||||
__all__ = ['CustomSerializer']
|
||||
|
||||
|
||||
class CustomSerializer(AssetSerializer):
|
||||
class Meta(AssetSerializer.Meta):
|
||||
model = Custom
|
|
@ -1,9 +1,9 @@
|
|||
from rest_framework.serializers import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from assets.models import Database
|
||||
from assets.serializers.gateway import GatewayWithAccountSecretSerializer
|
||||
from .common import AssetSerializer
|
||||
from ..gateway import GatewayWithAccountSecretSerializer
|
||||
|
||||
__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer']
|
||||
|
||||
|
|
|
@ -1,34 +1,18 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.models import Host
|
||||
from .common import AssetSerializer
|
||||
from .info.gathered import HostGatheredInfoSerializer
|
||||
|
||||
__all__ = ['HostInfoSerializer', 'HostSerializer']
|
||||
|
||||
|
||||
class HostInfoSerializer(serializers.Serializer):
|
||||
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
|
||||
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
|
||||
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
|
||||
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
|
||||
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
|
||||
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
|
||||
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
|
||||
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
|
||||
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
|
||||
|
||||
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
|
||||
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
|
||||
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
|
||||
__all__ = ['HostSerializer']
|
||||
|
||||
|
||||
class HostSerializer(AssetSerializer):
|
||||
info = HostInfoSerializer(required=False, label=_('Info'))
|
||||
gathered_info = HostGatheredInfoSerializer(required=False, read_only=True, label=_("Gathered info"))
|
||||
|
||||
class Meta(AssetSerializer.Meta):
|
||||
model = Host
|
||||
fields = AssetSerializer.Meta.fields + ['info']
|
||||
fields = AssetSerializer.Meta.fields + ['gathered_info']
|
||||
extra_kwargs = {
|
||||
**AssetSerializer.Meta.extra_kwargs,
|
||||
'address': {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class HostGatheredInfoSerializer(serializers.Serializer):
|
||||
vendor = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Vendor'))
|
||||
model = serializers.CharField(max_length=54, required=False, allow_blank=True, label=_('Model'))
|
||||
sn = serializers.CharField(max_length=128, required=False, allow_blank=True, label=_('Serial number'))
|
||||
cpu_model = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('CPU model'))
|
||||
cpu_count = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU count'))
|
||||
cpu_cores = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU cores'))
|
||||
cpu_vcpus = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('CPU vcpus'))
|
||||
memory = serializers.CharField(max_length=64, allow_blank=True, required=False, label=_('Memory'))
|
||||
disk_total = serializers.CharField(max_length=1024, allow_blank=True, required=False, label=_('Disk total'))
|
||||
|
||||
distribution = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('OS'))
|
||||
distribution_version = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS version'))
|
||||
arch = serializers.CharField(max_length=16, allow_blank=True, required=False, label=_('OS arch'))
|
||||
|
||||
|
||||
category_gathered_serializer_map = {
|
||||
'host': HostGatheredInfoSerializer,
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from assets.models import Database, Web
|
||||
|
||||
|
||||
class DatabaseSpecSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Database
|
||||
fields = ['db_name', 'use_ssl', 'allow_invalid_cert']
|
||||
|
||||
|
||||
class WebSpecSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Web
|
||||
fields = [
|
||||
'autofill', 'username_selector', 'password_selector',
|
||||
'submit_selector', 'script'
|
||||
]
|
||||
|
||||
|
||||
category_spec_serializer_map = {
|
||||
'database': DatabaseSpecSerializer,
|
||||
'web': WebSpecSerializer,
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from .asset import HostSerializer
|
||||
from .asset.common import AccountSecretSerializer
|
||||
from .asset.host import HostSerializer
|
||||
from ..models import Gateway, Asset
|
||||
|
||||
__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer']
|
||||
|
|
|
@ -4,6 +4,7 @@ from rest_framework import serializers
|
|||
from assets.const.web import FillType
|
||||
from common.serializers import WritableNestedModelSerializer
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.utils import lazyproperty
|
||||
from ..const import Category, AllTypes
|
||||
from ..models import Platform, PlatformProtocol, PlatformAutomation
|
||||
|
||||
|
@ -37,7 +38,6 @@ class ProtocolSettingSerializer(serializers.Serializer):
|
|||
default="", allow_blank=True, label=_("Submit selector")
|
||||
)
|
||||
script = serializers.JSONField(default=list, label=_("Script"))
|
||||
|
||||
# Redis
|
||||
auth_username = serializers.BooleanField(default=False, label=_("Auth with username"))
|
||||
|
||||
|
@ -87,6 +87,21 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
class PlatformCustomField(serializers.Serializer):
|
||||
TYPE_CHOICES = [
|
||||
("str", "str"),
|
||||
("int", "int"),
|
||||
("bool", "bool"),
|
||||
("choice", "choice"),
|
||||
]
|
||||
name = serializers.CharField(label=_("Name"), max_length=128)
|
||||
label = serializers.CharField(label=_("Label"), max_length=128)
|
||||
type = serializers.ChoiceField(choices=TYPE_CHOICES, label=_("Type"), default='str')
|
||||
default = serializers.CharField(default="", allow_blank=True, label=_("Default"), max_length=1024)
|
||||
help_text = serializers.CharField(default="", allow_blank=True, label=_("Help text"), max_length=1024)
|
||||
choices = serializers.ListField(default=list, label=_("Choices"), required=False)
|
||||
|
||||
|
||||
class PlatformSerializer(WritableNestedModelSerializer):
|
||||
SU_METHOD_CHOICES = [
|
||||
("sudo", "sudo su -"),
|
||||
|
@ -95,19 +110,16 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
|||
("super", "super 15"),
|
||||
("super_level", "super level 15")
|
||||
]
|
||||
|
||||
charset = LabeledChoiceField(
|
||||
choices=Platform.CharsetChoices.choices, label=_("Charset")
|
||||
)
|
||||
charset = LabeledChoiceField(choices=Platform.CharsetChoices.choices, label=_("Charset"), default='utf-8')
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), label=_("Type"))
|
||||
category = LabeledChoiceField(choices=Category.choices, label=_("Category"))
|
||||
protocols = PlatformProtocolSerializer(
|
||||
label=_("Protocols"), many=True, required=False
|
||||
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
|
||||
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
|
||||
su_method = LabeledChoiceField(
|
||||
choices=SU_METHOD_CHOICES, label=_("Su method"),
|
||||
required=False, default="sudo", allow_null=True
|
||||
)
|
||||
automation = PlatformAutomationSerializer(label=_("Automation"), required=False)
|
||||
su_method = LabeledChoiceField(choices=SU_METHOD_CHOICES,
|
||||
label=_("Su method"), required=False, default="sudo", allow_null=True
|
||||
)
|
||||
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
|
@ -115,19 +127,54 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
|||
fields_small = fields_mini + [
|
||||
"category", "type", "charset",
|
||||
]
|
||||
fields_other = [
|
||||
'date_created', 'date_updated', 'created_by', 'updated_by',
|
||||
read_only_fields = [
|
||||
'internal', 'date_created', 'date_updated',
|
||||
'created_by', 'updated_by'
|
||||
]
|
||||
fields = fields_small + [
|
||||
"protocols", "domain_enabled", "su_enabled",
|
||||
"su_method", "automation", "comment",
|
||||
] + fields_other
|
||||
"su_method", "automation", "comment", "custom_fields",
|
||||
] + read_only_fields
|
||||
extra_kwargs = {
|
||||
"su_enabled": {"label": _('Su enabled')},
|
||||
"domain_enabled": {"label": _('Domain enabled')},
|
||||
"domain_default": {"label": _('Default Domain')},
|
||||
}
|
||||
|
||||
@property
|
||||
def platform_category_type(self):
|
||||
if self.instance:
|
||||
return self.instance.category, self.instance.type
|
||||
if self.initial_data:
|
||||
return self.initial_data.get('category'), self.initial_data.get('type')
|
||||
raise serializers.ValidationError({'type': _("type is required")})
|
||||
|
||||
def add_type_choices(self, name, label):
|
||||
tp = self.fields['type']
|
||||
tp.choices[name] = label
|
||||
tp.choice_mapper[name] = label
|
||||
tp.choice_strings_to_values[name] = label
|
||||
|
||||
@lazyproperty
|
||||
def constraints(self):
|
||||
category, tp = self.platform_category_type
|
||||
constraints = AllTypes.get_constraints(category, tp)
|
||||
return constraints
|
||||
|
||||
def validate(self, attrs):
|
||||
domain_enabled = attrs.get('domain_enabled', False) and self.constraints.get('domain_enabled', False)
|
||||
su_enabled = attrs.get('su_enabled', False) and self.constraints.get('su_enabled', False)
|
||||
automation = attrs.get('automation', {})
|
||||
automation['ansible_enabled'] = automation.get('ansible_enabled', False) \
|
||||
and self.constraints.get('ansible_enabled', False)
|
||||
attrs.update({
|
||||
'domain_enabled': domain_enabled,
|
||||
'su_enabled': su_enabled,
|
||||
'automation': automation,
|
||||
})
|
||||
self.initial_data['automation'] = automation
|
||||
return attrs
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.prefetch_related(
|
||||
|
|
|
@ -66,11 +66,11 @@ def on_asset_create(sender, instance=None, created=False, **kwargs):
|
|||
ensure_asset_has_node(assets=(instance,))
|
||||
|
||||
# 获取资产硬件信息
|
||||
auto_info = instance.auto_info
|
||||
if auto_info.get('ping_enabled'):
|
||||
auto_config = instance.auto_config
|
||||
if auto_config.get('ping_enabled'):
|
||||
logger.debug('Asset {} ping enabled, test connectivity'.format(instance.name))
|
||||
test_assets_connectivity_handler(assets=(instance,))
|
||||
if auto_info.get('gather_facts_enabled'):
|
||||
if auto_config.get('gather_facts_enabled'):
|
||||
logger.debug('Asset {} gather facts enabled, gather facts'.format(instance.name))
|
||||
gather_assets_facts_handler(assets=(instance,))
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device')
|
|||
router.register(r'databases', api.DatabaseViewSet, 'database')
|
||||
router.register(r'webs', api.WebViewSet, 'web')
|
||||
router.register(r'clouds', api.CloudViewSet, 'cloud')
|
||||
router.register(r'customs', api.CustomViewSet, 'custom')
|
||||
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
|
||||
router.register(r'labels', api.LabelViewSet, 'label')
|
||||
router.register(r'nodes', api.NodeViewSet, 'node')
|
||||
|
|
|
@ -5,7 +5,8 @@ from accounts.const import SecretType
|
|||
from accounts.models import Account
|
||||
from acls.models import CommandGroup, CommandFilterACL
|
||||
from assets.models import Asset, Platform, Gateway, Domain
|
||||
from assets.serializers import PlatformSerializer, AssetProtocolsSerializer
|
||||
from assets.serializers.asset import AssetProtocolsSerializer
|
||||
from assets.serializers.platform import PlatformSerializer
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
|
@ -30,14 +31,12 @@ class _ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'id', 'name', 'address', 'protocols',
|
||||
'category', 'type', 'org_id', 'spec_info',
|
||||
'secret_info',
|
||||
'id', 'name', 'address', 'protocols', 'category',
|
||||
'type', 'org_id', 'spec_info', 'secret_info',
|
||||
]
|
||||
|
||||
|
||||
class _SimpleAccountSerializer(serializers.ModelSerializer):
|
||||
""" Account """
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
|
@ -46,20 +45,18 @@ class _SimpleAccountSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
|
||||
""" Account """
|
||||
su_from = _SimpleAccountSerializer(required=False, label=_('Su from'))
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, required=False, label=_('Secret type'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'id', 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged'
|
||||
'id', 'name', 'username', 'secret_type',
|
||||
'secret', 'su_from', 'privileged'
|
||||
]
|
||||
|
||||
|
||||
class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||
""" Gateway """
|
||||
|
||||
account = _SimpleAccountSerializer(
|
||||
required=False, source='select_account', read_only=True
|
||||
)
|
||||
|
@ -85,7 +82,8 @@ class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = CommandFilterACL
|
||||
fields = [
|
||||
'id', 'name', 'command_groups', 'action', 'reviewers', 'priority', 'is_active'
|
||||
'id', 'name', 'command_groups', 'action',
|
||||
'reviewers', 'priority', 'is_active'
|
||||
]
|
||||
|
||||
|
||||
|
@ -136,8 +134,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
|
|||
'id', 'value', 'user', 'asset', 'account',
|
||||
'platform', 'command_filter_acls', 'protocol',
|
||||
'domain', 'gateway', 'actions', 'expire_at',
|
||||
'from_ticket',
|
||||
'expire_now', 'connect_method',
|
||||
'from_ticket', 'expire_now', 'connect_method',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'value': {'read_only': True},
|
||||
|
|
|
@ -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
|
||||
oid sha256:15e96f9f31e92077ac828e248a30678e53b7c867757ae6348ae9805bc64874bc
|
||||
size 138124
|
||||
oid sha256:975e9e264596ef5f7233fc1d2fb45281a5fe13f5a722fc2b9d5c40562ada069d
|
||||
size 138303
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43695645a64669ba25c4fdfd413ce497a07592c320071b399cbb4f54466441e3
|
||||
size 113361
|
||||
oid sha256:035f9429613b541f229855a7d36c98e5f4736efce54dcd21119660dd6d89d94e
|
||||
size 114269
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -64,13 +64,7 @@ class DownloadUploadMixin:
|
|||
if instance and not update:
|
||||
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
|
||||
|
||||
serializer = serializers.AppletSerializer(data=manifest, instance=instance)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
save_to = default_storage.path('applets/{}'.format(name))
|
||||
if os.path.exists(save_to):
|
||||
shutil.rmtree(save_to)
|
||||
shutil.move(tmp_dir, save_to)
|
||||
serializer.save()
|
||||
applet, serializer = Applet.install_from_dir(tmp_dir)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
|
|
|
@ -12,7 +12,6 @@ from rest_framework.serializers import ValidationError
|
|||
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -91,24 +90,48 @@ class Applet(JMSBaseModel):
|
|||
return manifest
|
||||
|
||||
@classmethod
|
||||
def install_from_dir(cls, path):
|
||||
def load_platform_if_need(cls, d):
|
||||
from assets.serializers import PlatformSerializer
|
||||
|
||||
if not os.path.exists(os.path.join(d, 'platform.yml')):
|
||||
return
|
||||
try:
|
||||
with open(os.path.join(d, 'platform.yml')) as f:
|
||||
data = yaml.safe_load(f)
|
||||
except Exception as e:
|
||||
raise ValidationError({'error': _('Load platform.yml failed: {}').format(e)})
|
||||
|
||||
if data['category'] != 'custom':
|
||||
raise ValidationError({'error': _('Only support custom platform')})
|
||||
|
||||
try:
|
||||
tp = data['type']
|
||||
except KeyError:
|
||||
raise ValidationError({'error': _('Missing type in platform.yml')})
|
||||
|
||||
s = PlatformSerializer(data=data)
|
||||
s.add_type_choices(tp, tp)
|
||||
s.is_valid(raise_exception=True)
|
||||
s.save()
|
||||
|
||||
@classmethod
|
||||
def install_from_dir(cls, path, builtin=True):
|
||||
from terminal.serializers import AppletSerializer
|
||||
|
||||
manifest = cls.validate_pkg(path)
|
||||
name = manifest['name']
|
||||
if not has_valid_xpack_license() and name.lower() in ('navicat',):
|
||||
return
|
||||
|
||||
instance = cls.objects.filter(name=name).first()
|
||||
serializer = AppletSerializer(instance=instance, data=manifest)
|
||||
serializer.is_valid()
|
||||
serializer.save(builtin=True)
|
||||
pkg_path = default_storage.path('applets/{}'.format(name))
|
||||
serializer.save(builtin=builtin)
|
||||
|
||||
cls.load_platform_if_need(path)
|
||||
|
||||
pkg_path = default_storage.path('applets/{}'.format(name))
|
||||
if os.path.exists(pkg_path):
|
||||
shutil.rmtree(pkg_path)
|
||||
shutil.copytree(path, pkg_path)
|
||||
return instance
|
||||
return instance, serializer
|
||||
|
||||
def select_host_account(self):
|
||||
# 选择激活的发布机
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import time
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
|
@ -139,6 +141,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel):
|
|||
if self.user:
|
||||
setattr(self.user, SKIP_SIGNAL, True)
|
||||
self.user.delete()
|
||||
self.name = self.name + '_' + uuid.uuid4().hex[:8]
|
||||
self.user = None
|
||||
self.is_deleted = True
|
||||
self.save()
|
||||
|
|
Loading…
Reference in New Issue