diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py index cb1419636..cad913e6b 100644 --- a/apps/assets/const/base.py +++ b/apps/assets/const/base.py @@ -1,30 +1,51 @@ +from django.db.models import TextChoices + +from .protocol import Protocol -class ConstrainMixin: - def get_constrains(self): - pass +class BaseType(TextChoices): + """ + 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, 或者开启了也没有作用, 比如 k8s 开启了 domain,目前还不支持 + """ + @classmethod + def get_constrains(cls): + constrains = {} - def _get_category_constrains(self) -> dict: - raise NotImplementedError + base = cls._get_base_constrains() + protocols = cls._get_protocol_constrains() + automation = cls._get_automation_constrains() - def _get_protocol_constrains(self) -> dict: - raise NotImplementedError + base_default = base.pop('*', {}) + protocols_default = protocols.pop('*', {}) + automation_default = automation.pop('*', {}) - def _get_automation_constrains(self) -> dict: + for k, v in cls.choices: + tp_base = {**base_default, **base.get(k, {})} + tp_auto = {**automation_default, **automation.get(k, {})} + tp_protocols = {**protocols_default, **protocols.get(k, {})} + tp_protocols = cls._parse_protocols(tp_protocols, k) + tp_constrains = {**tp_base, 'protocols': tp_protocols, 'automation': tp_auto} + constrains[k] = tp_constrains + return constrains + + @classmethod + def _parse_protocols(cls, protocol, tp): + default_ports = Protocol.default_ports() + choices = protocol.get('choices', []) + if choices == '__self__': + choices = [tp] + protocols = [{'name': name, 'port': default_ports.get(name, 0)} for name in choices] + return protocols + + @classmethod + def _get_base_constrains(cls) -> dict: raise NotImplementedError @classmethod - def platform_constraints(cls): - return { - 'domain_enabled': False, - 'su_enabled': False, - 'brand_enabled': False, - 'ping_enabled': False, - 'gather_facts_enabled': False, - 'change_password_enabled': False, - 'verify_account_enabled': False, - 'create_account_enabled': False, - 'gather_accounts_enabled': False, - '_protocols': [] - } + def _get_protocol_constrains(cls) -> dict: + raise NotImplementedError + + @classmethod + def _get_automation_constrains(cls) -> dict: + raise NotImplementedError diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py index db3c96ebc..9e76946f3 100644 --- a/apps/assets/const/category.py +++ b/apps/assets/const/category.py @@ -1,17 +1,13 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from common.db.models import IncludesTextChoicesMeta, ChoicesMixin +from common.db.models import ChoicesMixin - -__all__ = [ - 'Category', 'ConstrainMixin' -] +__all__ = ['Category'] - -class Category(ConstrainMixin, ChoicesMixin, models.TextChoices): +class Category(ChoicesMixin, models.TextChoices): HOST = 'host', _('Host') DEVICE = 'device', _("Device") DATABASE = 'database', _("Database") diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py index 95e0be171..81819b034 100644 --- a/apps/assets/const/cloud.py +++ b/apps/assets/const/cloud.py @@ -1,31 +1,40 @@ -from django.db import models - -from common.db.models import ChoicesMixin +from .base import BaseType -from .category import ConstrainMixin - - -class CloudTypes(ConstrainMixin, ChoicesMixin, models.TextChoices): +class CloudTypes(BaseType): + PUBLIC = 'public', 'Public cloud' + PRIVATE = 'private', 'Private cloud' K8S = 'k8s', 'Kubernetes' - def category_constrains(self): + @classmethod + def _get_base_constrains(cls) -> dict: return { - 'domain_enabled': False, - 'su_enabled': False, - 'ping_enabled': False, - 'gather_facts_enabled': False, - 'verify_account_enabled': False, - 'change_password_enabled': False, - 'create_account_enabled': False, - 'gather_accounts_enabled': False, - '_protocols': [] + '*': { + 'domain_enabled': False, + 'su_enabled': False, + } } @classmethod - def platform_constraints(cls): - return { - cls.K8S: { - '_protocols': ['k8s'] + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'gather_facts_enabled': False, + 'verify_account_enabled': False, + 'change_password_enabled': False, + 'create_account_enabled': False, + 'gather_accounts_enabled': False, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['http', 'api'], + }, + cls.K8S: { + 'choices': ['k8s'] } } diff --git a/apps/assets/const/database.py b/apps/assets/const/database.py index a80b89529..dd8026359 100644 --- a/apps/assets/const/database.py +++ b/apps/assets/const/database.py @@ -1,6 +1,8 @@ +from .base import BaseType + -class DatabaseTypes(ConstrainMixin, ChoicesMixin, models.TextChoices): +class DatabaseTypes(BaseType): MYSQL = 'mysql', 'MySQL' MARIADB = 'mariadb', 'MariaDB' POSTGRESQL = 'postgresql', 'PostgreSQL' @@ -9,28 +11,33 @@ class DatabaseTypes(ConstrainMixin, ChoicesMixin, models.TextChoices): MONGODB = 'mongodb', 'MongoDB' REDIS = 'redis', 'Redis' - def category_constrains(self): + @classmethod + def _get_base_constrains(cls) -> dict: return { - 'domain_enabled': True, - 'su_enabled': False, - 'gather_facts_enabled': True, - 'verify_account_enabled': True, - 'change_password_enabled': True, - 'create_account_enabled': True, - 'gather_accounts_enabled': True, - '_protocols': [] + '*': { + 'domain_enabled': True, + 'su_enabled': False, + } } @classmethod - def platform_constraints(cls): - meta = {} - for name, label in cls.choices: - meta[name] = { - '_protocols': [name], - 'gather_facts_method': f'gather_facts_{name}', - 'verify_account_method': f'verify_account_{name}', - 'change_password_method': f'change_password_{name}', - 'create_account_method': f'create_account_{name}', - 'gather_accounts_method': f'gather_accounts_{name}', + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'gather_facts_enabled': True, + 'gather_accounts_enabled': True, + 'verify_account_enabled': True, + 'change_password_enabled': True, + 'create_account_enabled': True, } - return meta + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': '__self__', + } + } + diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py index 488806b3d..a3c325341 100644 --- a/apps/assets/const/device.py +++ b/apps/assets/const/device.py @@ -1,30 +1,40 @@ +from django.utils.translation import gettext_lazy as _ + +from .base import BaseType -class DeviceTypes(ConstrainMixin, ChoicesMixin, models.TextChoices): +class DeviceTypes(BaseType): GENERAL = 'general', _("General device") SWITCH = 'switch', _("Switch") ROUTER = 'router', _("Router") FIREWALL = 'firewall', _("Firewall") @classmethod - def category_constrains(cls): + def _get_base_constrains(cls) -> dict: return { - 'domain_enabled': True, - 'brand_enabled': True, - 'brands': [ - ('huawei', 'Huawei'), - ('cisco', 'Cisco'), - ('juniper', 'Juniper'), - ('h3c', 'H3C'), - ('dell', 'Dell'), - ('other', 'Other'), - ], - 'su_enabled': False, - 'ping_enabled': True, 'ping_method': 'ping', - 'gather_facts_enabled': False, - 'verify_account_enabled': False, - 'change_password_enabled': False, - 'create_account_enabled': False, - 'gather_accounts_enabled': False, - '_protocols': ['ssh', 'telnet'] + '*': { + 'domain_enabled': True, + 'su_enabled': False, + } + } + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['ssh', 'telnet'] + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + return { + '*': { + 'ping_enabled': True, + 'gather_facts_enabled': False, + 'gather_accounts_enabled': False, + 'verify_account_enabled': False, + 'change_password_enabled': False, + 'create_account_enabled': False, + } } diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index 1ffc0e728..f317fa292 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -1,40 +1,44 @@ +from .base import BaseType -class HostTypes(ConstrainMixin, ChoicesMixin, models.TextChoices): + +class HostTypes(BaseType): LINUX = 'linux', 'Linux' WINDOWS = 'windows', 'Windows' UNIX = 'unix', 'Unix' - OTHER_HOST = 'other', _("Other") + OTHER_HOST = 'other', "Other" - @staticmethod - def category_constrains(): + @classmethod + def _get_base_constrains(cls) -> dict: return { - 'domain_enabled': True, - 'su_enabled': True, 'su_method': 'sudo', - 'ping_enabled': True, 'ping_method': 'ping', - 'gather_facts_enabled': True, 'gather_facts_method': 'gather_facts_posix', - 'verify_account_enabled': True, 'verify_account_method': 'verify_account_posix', - 'change_password_enabled': True, 'change_password_method': 'change_password_posix', - 'create_account_enabled': True, 'create_account_method': 'create_account_posix', - 'gather_accounts_enabled': True, 'gather_accounts_method': 'gather_accounts_posix', - '_protocols': ['ssh', 'telnet'], + '*': { + 'domain_enabled': True, + 'su_enabled': True, + }, + cls.WINDOWS: { + 'su_enabled': False, + }, + cls.OTHER_HOST: { + 'su_enabled': False, + } } @classmethod - def platform_constraints(cls): + def _get_protocol_constrains(cls) -> dict: return { - cls.LINUX: { - '_protocols': ['ssh', 'rdp', 'vnc', 'telnet'] - }, - cls.WINDOWS: { - 'gather_facts_method': 'gather_facts_windows', - 'verify_account_method': 'verify_account_windows', - 'change_password_method': 'change_password_windows', - 'create_account_method': 'create_account_windows', - 'gather_accounts_method': 'gather_accounts_windows', - '_protocols': ['rdp', 'ssh', 'vnc'], - 'su_enabled': False - }, - cls.UNIX: { - '_protocols': ['ssh', 'vnc'] + '*': { + 'choices': ['ssh', 'telnet', 'vnc', 'rdp'] } - } \ No newline at end of file + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + return { + '*': { + 'ping_enabled': True, + 'gather_facts_enabled': True, + 'gather_accounts_enabled': True, + 'verify_account_enabled': True, + 'change_password_enabled': True, + 'create_account_enabled': True, + } + } diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index e19a7c28d..ebee189aa 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -1,12 +1,14 @@ from common.db.models import IncludesTextChoicesMeta, ChoicesMixin from common.tree import TreeNode +from .base import BaseType from .category import Category from .host import HostTypes from .device import DeviceTypes from .database import DatabaseTypes from .web import WebTypes from .cloud import CloudTypes +from .protocol import Protocol class AllTypes(ChoicesMixin, metaclass=IncludesTextChoicesMeta): @@ -18,24 +20,11 @@ class AllTypes(ChoicesMixin, metaclass=IncludesTextChoicesMeta): @classmethod def get_constraints(cls, category, tp): - constraints = ConstrainMixin.platform_constraints() - category_constraints = Category.platform_constraints().get(category) or {} - constraints.update(category_constraints) - types_cls = dict(cls.category_types()).get(category) if not types_cls: - return constraints - type_constraints = types_cls.platform_constraints().get(tp) or {} - constraints.update(type_constraints) - - _protocols = constraints.pop('_protocols', []) - default_ports = Protocol.default_ports() - protocols = [] - for p in _protocols: - port = default_ports.get(p, 0) - protocols.append({'name': p, 'port': port}) - constraints['protocols'] = protocols - return constraints + return {} + type_constraints = types_cls.get_constrains() + return type_constraints.get(tp, {}) @classmethod def category_types(cls): diff --git a/apps/assets/const/web.py b/apps/assets/const/web.py index f9bcffef4..48c575909 100644 --- a/apps/assets/const/web.py +++ b/apps/assets/const/web.py @@ -1,16 +1,37 @@ +from django.utils.translation import gettext_lazy as _ -class WebTypes(ConstrainMixin, ChoicesMixin, models.TextChoices): +from .base import BaseType + + +class WebTypes(BaseType): WEBSITE = 'website', _('General website') - def category_constrains(self): + @classmethod + def _get_base_constrains(cls) -> dict: return { - 'domain_enabled': False, - 'su_enabled': False, - 'ping_enabled': False, - 'gather_facts_enabled': False, - 'verify_account_enabled': False, - 'change_password_enabled': False, - 'create_account_enabled': False, - 'gather_accounts_enabled': False, - '_protocols': ['http', 'https'] - } \ No newline at end of file + '*': { + 'domain_enabled': False, + 'su_enabled': False, + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'gather_facts_enabled': False, + 'verify_account_enabled': False, + 'change_password_enabled': False, + 'create_account_enabled': False, + 'gather_accounts_enabled': False, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': ['http', 'api'], + } + } diff --git a/apps/assets/migrations/0101_auto_20220803_1448.py b/apps/assets/migrations/0101_auto_20220803_1448.py index 26f4dc36b..bfd07a7aa 100644 --- a/apps/assets/migrations/0101_auto_20220803_1448.py +++ b/apps/assets/migrations/0101_auto_20220803_1448.py @@ -10,14 +10,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Protocol', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=32, verbose_name='Name')), - ('port', models.IntegerField(verbose_name='Port')), - ], - ), migrations.RemoveField( model_name='asset', name='port', @@ -26,24 +18,24 @@ class Migration(migrations.Migration): model_name='asset', name='protocol', ), - migrations.AddField( + migrations.RenameField( model_name='asset', - name='_protocols', - field=models.CharField(blank=True, default='ssh/22', max_length=128, verbose_name='Protocols'), - ), - migrations.RemoveField( - model_name='asset', - name='protocols', + old_name='protocols', + new_name='_protocols', ), migrations.AlterField( model_name='systemuser', name='protocol', field=models.CharField(default='ssh', max_length=16, verbose_name='Protocol'), ), - migrations.AddField( - model_name='asset', - name='protocols', - field=models.ManyToManyField(blank=True, to='assets.Protocol', verbose_name='Protocols'), + migrations.CreateModel( + name='Protocol', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='Name')), + ('port', models.IntegerField(verbose_name='Port')), + ('asset', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.asset', verbose_name='Asset')), + ], ), migrations.DeleteModel( name='Cluster', diff --git a/apps/assets/migrations/0102_auto_20220803_1859.py b/apps/assets/migrations/0102_auto_20220803_1859.py index e220da05a..266d9b37b 100644 --- a/apps/assets/migrations/0102_auto_20220803_1859.py +++ b/apps/assets/migrations/0102_auto_20220803_1859.py @@ -31,7 +31,7 @@ def migrate_asset_protocols(apps, schema_editor): protocol = protocol_map.get(name_port) if not protocol: protocol = protocol_model.objects.get_or_create( - defaults={'name': name, 'port': port}, + defaults={'name': name, 'port': port, 'asset': asset}, name=name, port=port )[0] assets_protocols.append(asset_protocol_through(asset_id=asset.id, protocol_id=protocol.id)) diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 6b4bd31a1..e79b2fff5 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -13,6 +13,5 @@ from .backup import * from ._user import * # 废弃以下 # from ._authbook import * -from .protocol import * from .cmd_filter import * diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 76369b94c..7d2810da5 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -16,7 +16,7 @@ from orgs.mixins.models import OrgManager, JMSOrgBaseModel from ..platform import Platform from ..base import AbsConnectivity -__all__ = ['Asset', 'AssetQuerySet', 'default_node'] +__all__ = ['Asset', 'AssetQuerySet', 'default_node', 'Protocol'] logger = logging.getLogger(__name__) @@ -72,11 +72,16 @@ class NodesRelationMixin: return nodes +class Protocol(models.Model): + name = models.CharField(max_length=32, verbose_name=_("Name")) + port = models.IntegerField(verbose_name=_("Port")) + asset = models.ForeignKey('Asset', on_delete=models.CASCADE, related_name='protocols', verbose_name=_("Asset")) + + class Asset(AbsConnectivity, NodesRelationMixin, JMSOrgBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) - protocols = models.ManyToManyField('Protocol', verbose_name=_("Protocols"), blank=True) platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index a18c7ae5e..d9e7097a7 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -58,6 +58,8 @@ class BaseAccount(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_("Name")) username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) + secret_type = models.CharField(max_length=16, default='password', verbose_name=_('Secret type')) + secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) diff --git a/apps/assets/models/protocol.py b/apps/assets/models/protocol.py deleted file mode 100644 index d42ee2754..000000000 --- a/apps/assets/models/protocol.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - - -class Protocol(models.Model): - name = models.CharField(max_length=32, verbose_name=_("Name")) - port = models.IntegerField(verbose_name=_("Port")) diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index 2717547cd..325aa4f34 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -8,7 +8,7 @@ from django.db.models import F from common.drf.serializers import JMSWritableNestedModelSerializer from common.drf.fields import LabeledChoiceField, ObjectRelatedField from ..account import AccountSerializer -from ...models import Asset, Node, Platform, Protocol, Label, Domain, Account +from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol from ...const import Category, AllTypes __all__ = [ diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 785cf6c39..380c91f01 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -89,23 +89,6 @@ class PlatformSerializer(JMSWritableNestedModelSerializer): 'domain_default': {'label': "默认网域"}, } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_brand_choices() - - def set_brand_choices(self): - field = self.fields.get('brand') - request = self.context.get('request') - if not field or not request: - return - category = request.query_params.get('category', '') - constraints = Category.platform_constraints().get(category) - if not constraints: - return - field.choices = constraints.get('brands', []) - if field.choices: - field.required = True - class PlatformOpsMethodSerializer(serializers.Serializer): id = serializers.CharField(read_only=True)