Skip to content

Commit

Permalink
feat(backend): 标签功能 TencentBlueKing#6235
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangzhw8 authored and iSecloud committed Nov 12, 2024
1 parent 67a834e commit b2ce926
Show file tree
Hide file tree
Showing 18 changed files with 416 additions and 22 deletions.
8 changes: 5 additions & 3 deletions dbm-ui/backend/db_meta/enums/comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ class DBCCModule(str, StructuredEnum):


class TagType(str, StructuredEnum):
CUSTOM = EnumField("custom", _("custom"))
SYSTEM = EnumField("system", _("system"))
CUSTOM = EnumField("custom", _("自定义标签"))
SYSTEM = EnumField("system", _("系统标签"))
BUILTIN = EnumField("builtin", _("内置标签"))


class SystemTagEnum(str, StructuredEnum):
"""系统内置的tag名称"""

TEMPORARY = EnumField("temporary", _("temporary"))
TEMPORARY = EnumField("temporary", _("临时集群"))
RESOURCE_TAG = EnumField("resource", _("资源标签"))


class RedisVerUpdateNodeType(str, StructuredEnum):
Expand Down
84 changes: 84 additions & 0 deletions dbm-ui/backend/db_meta/migrations/0043_auto_20241015_2128.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Generated by Django 3.2.25 on 2024-10-15 13:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("db_meta", "0042_auto_20240903_1138"),
]

operations = [
migrations.AlterModelOptions(
name="bksubzone",
options={"verbose_name": "蓝鲸园区表(BKSubzone)", "verbose_name_plural": "蓝鲸园区表(BKSubzone)"},
),
migrations.AddField(
model_name="cluster",
name="tags",
field=models.ManyToManyField(blank=True, help_text="标签(外键)", to="db_meta.Tag"),
),
migrations.AddField(
model_name="tag",
name="key",
field=models.CharField(default="", help_text="标签键", max_length=64),
),
migrations.AddField(
model_name="tag",
name="value",
field=models.CharField(default="", help_text="标签值", max_length=255),
),
migrations.AlterField(
model_name="spec",
name="cpu",
field=models.JSONField(help_text='cpu规格描述:{"min":1,"max":10}', null=True),
),
migrations.AlterField(
model_name="spec",
name="device_class",
field=models.JSONField(help_text='实际机器机型: ["class1","class2"]', null=True),
),
migrations.AlterField(
model_name="spec",
name="mem",
field=models.JSONField(help_text='mem规格描述:{"min":100,"max":1000}', null=True),
),
migrations.AlterField(
model_name="spec",
name="qps",
field=models.JSONField(default=dict, help_text='qps规格描述:{"min": 1, "max": 100}'),
),
migrations.AlterField(
model_name="spec",
name="storage_spec",
field=models.JSONField(help_text='存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]', null=True),
),
migrations.AlterField(
model_name="tag",
name="bk_biz_id",
field=models.IntegerField(default=0, help_text="业务 ID"),
),
migrations.AlterField(
model_name="tag",
name="type",
field=models.CharField(
choices=[("custom", "自定义标签"), ("system", "系统标签"), ("builtin", "内置标签")],
default="custom",
help_text="tag类型",
max_length=64,
),
),
migrations.AlterUniqueTogether(
name="tag",
unique_together={("bk_biz_id", "key", "value")},
),
migrations.RemoveField(
model_name="tag",
name="cluster",
),
migrations.RemoveField(
model_name="tag",
name="name",
),
]
2 changes: 2 additions & 0 deletions dbm-ui/backend/db_meta/models/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
ClusterSqlserverStatusFlags,
)
from backend.db_meta.exceptions import ClusterExclusiveOperateException, DBMetaException
from backend.db_meta.models.tag import Tag
from backend.db_services.version.constants import LATEST, PredixyVersion, TwemproxyVersion
from backend.exceptions import ApiError
from backend.flow.consts import DEFAULT_RIAK_PORT
Expand All @@ -69,6 +70,7 @@ class Cluster(AuditedModel):
max_length=128, help_text=_("容灾要求"), choices=AffinityEnum.get_choices(), default=AffinityEnum.NONE.value
)
time_zone = models.CharField(max_length=16, default=DEFAULT_TIME_ZONE, help_text=_("集群所在的时区"))
tags = models.ManyToManyField(Tag, blank=True, help_text=_("标签(外键)"))

class Meta:
unique_together = [("bk_biz_id", "immute_domain", "cluster_type", "db_module_id"), ("immute_domain",)]
Expand Down
23 changes: 16 additions & 7 deletions dbm-ui/backend/db_meta/models/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,29 @@
from django.utils.translation import ugettext_lazy as _

from backend.bk_web.models import AuditedModel
from backend.configuration.constants import PLAT_BIZ_ID
from backend.db_meta.enums.comm import TagType
from backend.db_meta.models import Cluster


class Tag(AuditedModel):
bk_biz_id = models.IntegerField(default=0)
name = models.CharField(max_length=64, default="", help_text=_("tag名称"))
type = models.CharField(max_length=64, help_text=_("tag类型"), choices=TagType.get_choices())
cluster = models.ManyToManyField(Cluster, blank=True, help_text=_("关联集群"))
bk_biz_id = models.IntegerField(help_text=_("业务 ID"), default=0)
key = models.CharField(help_text=_("标签键"), default="", max_length=64)
value = models.CharField(help_text=_("标签值"), default="", max_length=255)
type = models.CharField(
help_text=_("tag类型"), max_length=64, choices=TagType.get_choices(), default=TagType.CUSTOM.value
)

class Meta:
unique_together = ["bk_biz_id", "name"]
unique_together = ["bk_biz_id", "key", "value"]

@property
def tag_desc(self):
"""仅返回tag的信息"""
return {"bk_biz_id": self.bk_biz_id, "name": self.name, "type": self.type}
return {"bk_biz_id": self.bk_biz_id, "key": self.key, "type": self.type}

@classmethod
def get_or_create_system_tag(cls, key: str, value: str):
tag, created = cls.objects.get_or_create(
bk_biz_id=PLAT_BIZ_ID, key=key, value=value, type=TagType.SYSTEM.value
)
return tag
13 changes: 10 additions & 3 deletions dbm-ui/backend/db_services/dbbase/resources/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def export_instance(cls, bk_biz_id: int, bk_host_ids: list) -> HttpResponse:
@classmethod
def get_temporary_cluster_info(cls, cluster, ticket_type):
"""如果当前集群是临时集群,则补充临时集群相关信息。"""
tags = [tag.name for tag in cluster.tag_set.all()]
tags = [tag.key for tag in cluster.tag_set.all()]
if SystemTagEnum.TEMPORARY.value not in tags:
return {}
record = ClusterOperateRecord.objects.filter(cluster_id=cluster.id, ticket__ticket_type=ticket_type).first()
Expand Down Expand Up @@ -401,8 +401,14 @@ def _list_clusters(
for param in filter_params_map:
if query_params.get(param):
query_filters &= filter_params_map[param]

# 对标签进行过滤,标签“且”查询,需以追加 filter 的方式实现
cluster_queryset = Cluster.objects.filter(query_filters)
for tag_id in query_params.get("tag_ids", "").split(","):
cluster_queryset = cluster_queryset.filter(tags__id=tag_id)

# 一join多的一方会有重复的数据,去重
cluster_queryset = Cluster.objects.filter(query_filters).distinct()
cluster_queryset = cluster_queryset.distinct()

# 实例筛选
def filter_instance_func(_query_params, _cluster_queryset, _proxy_queryset, _storage_queryset):
Expand Down Expand Up @@ -466,7 +472,7 @@ def _filter_cluster_hook(
Prefetch("proxyinstance_set", queryset=proxy_queryset.select_related("machine"), to_attr="proxies"),
Prefetch("storageinstance_set", queryset=storage_queryset.select_related("machine"), to_attr="storages"),
Prefetch("clusterentry_set", to_attr="entries"),
"tag_set",
"tags",
)
# 由于对 queryset 切片工作方式的模糊性,这里的values可能会获得非预期的排序,所以不要在切片后用values
# cluster_ids = list(cluster_queryset.values_list("id", flat=True))
Expand Down Expand Up @@ -562,6 +568,7 @@ def _to_cluster_representation(
"updater": cluster.updater,
"create_at": datetime2str(cluster.create_at),
"update_at": datetime2str(cluster.update_at),
"tags": [{tag.key: tag.value} for tag in cluster.tags.all()],
}

@classmethod
Expand Down
1 change: 1 addition & 0 deletions dbm-ui/backend/db_services/dbbase/resources/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ListResourceSLZ(serializers.Serializer):
bk_cloud_id = serializers.CharField(required=False, help_text=_("管控区域"))
cluster_type = serializers.CharField(required=False, help_text=_("集群类型"))
ordering = serializers.CharField(required=False, help_text=_("排序字段,非必填"))
tag_ids = serializers.CharField(required=False, help_text=_("标签"))


class ListMySQLResourceSLZ(ListResourceSLZ):
Expand Down
File renamed without changes.
17 changes: 17 additions & 0 deletions dbm-ui/backend/db_services/tag/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at https://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from django.utils.translation import gettext_lazy as _

from blue_krill.data_types.enum import EnumField, StructuredEnum


class TagResourceType(str, StructuredEnum):
DB_RESOURCE = EnumField("db_resource", _("资源池"))
100 changes: 100 additions & 0 deletions dbm-ui/backend/db_services/tag/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at https://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""
from typing import Dict, List

from django.db.models import ManyToManyRel
from django.utils.translation import gettext_lazy as _

from backend.db_meta.models import Tag
from backend.db_services.tag.constants import TagResourceType
from backend.exceptions import ValidationError


class TagHandler:
"""标签的操作类"""

def batch_set_tags(self, tag_ids: List[int]):
"""
给资源批量设置标签
"""
# 1. 判断标签中 key 是否允许多值

# 2. 批量设置标签
pass

@classmethod
def delete_tags(cls, bk_biz_id: int, ids: List[int]):
"""
删除标签
"""
# 1. 检查标签是否被引用
related_resources = cls.query_related_resources(ids)
for related_resource in related_resources:
if related_resource["count"] > 0:
raise ValidationError(_("标签被引用,无法删除"))

# 2. 批量删除标签
Tag.objects.filter(bk_biz_id=bk_biz_id, id__in=ids).delete()

@classmethod
def query_related_resources(cls, ids: List[int], resource_type: str = None):
"""
查询关联资源
"""
# 1. 查询外键关联资源
data = []
for tag_id in ids:
info = {"id": tag_id, "related_resources": []}
for field in Tag._meta.get_fields():
if isinstance(field, ManyToManyRel) and (field.name == resource_type or resource_type is None):
related_objs = field.related_model.objects.prefetch_related("tags").filter(tags__id=tag_id)
info["related_resources"].append(
{
"resource_type": field.name,
"count": related_objs.count(),
}
)

# 2. 查询第三方服务关联资源(如资源池、后续可能扩展的别的服务)
if resource_type == TagResourceType.DB_RESOURCE.value or resource_type is None:
info["related_resources"].append(
{
"resource_type": TagResourceType.DB_RESOURCE.value,
# TODO 请求资源池接口得到统计数量
"count": 0,
}
)
data.append(info)
return data

@classmethod
def batch_create(cls, bk_biz_id: int, tags: List[Dict[str, str]], creator: str = ""):
"""
批量创建标签
"""
duplicate_tags = cls.verify_duplicated(bk_biz_id, tags)
if duplicate_tags:
raise ValidationError(_("检查到重复的标签"), data=duplicate_tags)

tag_models = [Tag(bk_biz_id=bk_biz_id, key=tag["key"], value=tag["value"], creator=creator) for tag in tags]
Tag.objects.bulk_create(tag_models)

@classmethod
def verify_duplicated(cls, bk_biz_id: int, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""
检查标签是否重复
"""
biz_tags = [f"{tag.key}:{tag.value}" for tag in Tag.objects.filter(bk_biz_id=bk_biz_id)]
duplicate_tags = []
for tag in tags:
if f'{tag["key"]}:{tag["value"]}' in biz_tags:
duplicate_tags.append(tag)
return duplicate_tags
51 changes: 51 additions & 0 deletions dbm-ui/backend/db_services/tag/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at https://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""

from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from backend.bk_web.serializers import AuditedSerializer
from backend.db_meta.models import Tag


class TagSerializer(AuditedSerializer, serializers.ModelSerializer):
"""
标签序列化器
"""

class Meta:
model = Tag
fields = "__all__"


class BatchCreateTagsSerializer(serializers.Serializer):
class CreateTagSerializer(serializers.Serializer):
key = serializers.CharField(help_text=_("标签key"))
value = serializers.CharField(help_text=_("标签value"))

bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
tags = serializers.ListField(child=CreateTagSerializer())


class UpdateTagSerializer(serializers.Serializer):
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
id = serializers.IntegerField(help_text=_("标签 ID"))
value = serializers.CharField(help_text=_("标签value"))


class DeleteTagsSerializer(serializers.Serializer):
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
ids = serializers.ListSerializer(child=serializers.IntegerField(help_text=_("标签 ID")), help_text=_("标签 ID 列表"))


class QueryRelatedResourceSerializer(serializers.Serializer):
ids = serializers.ListSerializer(child=serializers.IntegerField(help_text=_("标签 ID")), help_text=_("标签 ID 列表"))
resource_type = serializers.CharField(help_text=_("资源类型"), required=False)
19 changes: 19 additions & 0 deletions dbm-ui/backend/db_services/tag/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at https://opensource.org/licenses/MIT
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
"""

from rest_framework.routers import DefaultRouter

from . import views

routers = DefaultRouter(trailing_slash=True)
routers.register("", views.TagViewSet, basename="tag")

urlpatterns = routers.urls
Loading

0 comments on commit b2ce926

Please sign in to comment.