Skip to content

Commit ae93f4f

Browse files
zhangzhw8iSecloud
authored andcommitted
feat(backend): 标签功能 #6235
1 parent ab513ed commit ae93f4f

File tree

18 files changed

+416
-22
lines changed

18 files changed

+416
-22
lines changed

dbm-ui/backend/db_meta/enums/comm.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,16 @@ class DBCCModule(str, StructuredEnum):
2626

2727

2828
class TagType(str, StructuredEnum):
29-
CUSTOM = EnumField("custom", _("custom"))
30-
SYSTEM = EnumField("system", _("system"))
29+
CUSTOM = EnumField("custom", _("自定义标签"))
30+
SYSTEM = EnumField("system", _("系统标签"))
31+
BUILTIN = EnumField("builtin", _("内置标签"))
3132

3233

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

36-
TEMPORARY = EnumField("temporary", _("temporary"))
37+
TEMPORARY = EnumField("temporary", _("临时集群"))
38+
RESOURCE_TAG = EnumField("resource", _("资源标签"))
3739

3840

3941
class RedisVerUpdateNodeType(str, StructuredEnum):
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Generated by Django 3.2.25 on 2024-10-15 13:28
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("db_meta", "0042_auto_20240903_1138"),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name="bksubzone",
15+
options={"verbose_name": "蓝鲸园区表(BKSubzone)", "verbose_name_plural": "蓝鲸园区表(BKSubzone)"},
16+
),
17+
migrations.AddField(
18+
model_name="cluster",
19+
name="tags",
20+
field=models.ManyToManyField(blank=True, help_text="标签(外键)", to="db_meta.Tag"),
21+
),
22+
migrations.AddField(
23+
model_name="tag",
24+
name="key",
25+
field=models.CharField(default="", help_text="标签键", max_length=64),
26+
),
27+
migrations.AddField(
28+
model_name="tag",
29+
name="value",
30+
field=models.CharField(default="", help_text="标签值", max_length=255),
31+
),
32+
migrations.AlterField(
33+
model_name="spec",
34+
name="cpu",
35+
field=models.JSONField(help_text='cpu规格描述:{"min":1,"max":10}', null=True),
36+
),
37+
migrations.AlterField(
38+
model_name="spec",
39+
name="device_class",
40+
field=models.JSONField(help_text='实际机器机型: ["class1","class2"]', null=True),
41+
),
42+
migrations.AlterField(
43+
model_name="spec",
44+
name="mem",
45+
field=models.JSONField(help_text='mem规格描述:{"min":100,"max":1000}', null=True),
46+
),
47+
migrations.AlterField(
48+
model_name="spec",
49+
name="qps",
50+
field=models.JSONField(default=dict, help_text='qps规格描述:{"min": 1, "max": 100}'),
51+
),
52+
migrations.AlterField(
53+
model_name="spec",
54+
name="storage_spec",
55+
field=models.JSONField(help_text='存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]', null=True),
56+
),
57+
migrations.AlterField(
58+
model_name="tag",
59+
name="bk_biz_id",
60+
field=models.IntegerField(default=0, help_text="业务 ID"),
61+
),
62+
migrations.AlterField(
63+
model_name="tag",
64+
name="type",
65+
field=models.CharField(
66+
choices=[("custom", "自定义标签"), ("system", "系统标签"), ("builtin", "内置标签")],
67+
default="custom",
68+
help_text="tag类型",
69+
max_length=64,
70+
),
71+
),
72+
migrations.AlterUniqueTogether(
73+
name="tag",
74+
unique_together={("bk_biz_id", "key", "value")},
75+
),
76+
migrations.RemoveField(
77+
model_name="tag",
78+
name="cluster",
79+
),
80+
migrations.RemoveField(
81+
model_name="tag",
82+
name="name",
83+
),
84+
]

dbm-ui/backend/db_meta/models/cluster.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
ClusterSqlserverStatusFlags,
4545
)
4646
from backend.db_meta.exceptions import ClusterExclusiveOperateException, DBMetaException
47+
from backend.db_meta.models.tag import Tag
4748
from backend.db_services.version.constants import LATEST, PredixyVersion, TwemproxyVersion
4849
from backend.exceptions import ApiError
4950
from backend.flow.consts import DEFAULT_RIAK_PORT
@@ -69,6 +70,7 @@ class Cluster(AuditedModel):
6970
max_length=128, help_text=_("容灾要求"), choices=AffinityEnum.get_choices(), default=AffinityEnum.NONE.value
7071
)
7172
time_zone = models.CharField(max_length=16, default=DEFAULT_TIME_ZONE, help_text=_("集群所在的时区"))
73+
tags = models.ManyToManyField(Tag, blank=True, help_text=_("标签(外键)"))
7274

7375
class Meta:
7476
unique_together = [("bk_biz_id", "immute_domain", "cluster_type", "db_module_id"), ("immute_domain",)]

dbm-ui/backend/db_meta/models/tag.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,29 @@
1212
from django.utils.translation import ugettext_lazy as _
1313

1414
from backend.bk_web.models import AuditedModel
15+
from backend.configuration.constants import PLAT_BIZ_ID
1516
from backend.db_meta.enums.comm import TagType
16-
from backend.db_meta.models import Cluster
1717

1818

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

2527
class Meta:
26-
unique_together = ["bk_biz_id", "name"]
28+
unique_together = ["bk_biz_id", "key", "value"]
2729

2830
@property
2931
def tag_desc(self):
3032
"""仅返回tag的信息"""
31-
return {"bk_biz_id": self.bk_biz_id, "name": self.name, "type": self.type}
33+
return {"bk_biz_id": self.bk_biz_id, "key": self.key, "type": self.type}
34+
35+
@classmethod
36+
def get_or_create_system_tag(cls, key: str, value: str):
37+
tag, created = cls.objects.get_or_create(
38+
bk_biz_id=PLAT_BIZ_ID, key=key, value=value, type=TagType.SYSTEM.value
39+
)
40+
return tag

dbm-ui/backend/db_services/dbbase/resources/query.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def export_instance(cls, bk_biz_id: int, bk_host_ids: list) -> HttpResponse:
223223
@classmethod
224224
def get_temporary_cluster_info(cls, cluster, ticket_type):
225225
"""如果当前集群是临时集群,则补充临时集群相关信息。"""
226-
tags = [tag.name for tag in cluster.tag_set.all()]
226+
tags = [tag.key for tag in cluster.tag_set.all()]
227227
if SystemTagEnum.TEMPORARY.value not in tags:
228228
return {}
229229
record = ClusterOperateRecord.objects.filter(cluster_id=cluster.id, ticket__ticket_type=ticket_type).first()
@@ -411,8 +411,14 @@ def _list_clusters(
411411
for param in filter_params_map:
412412
if query_params.get(param):
413413
query_filters &= filter_params_map[param]
414+
415+
# 对标签进行过滤,标签“且”查询,需以追加 filter 的方式实现
416+
cluster_queryset = Cluster.objects.filter(query_filters)
417+
for tag_id in query_params.get("tag_ids", "").split(","):
418+
cluster_queryset = cluster_queryset.filter(tags__id=tag_id)
419+
414420
# 一join多的一方会有重复的数据,去重
415-
cluster_queryset = Cluster.objects.filter(query_filters).distinct()
421+
cluster_queryset = cluster_queryset.distinct()
416422

417423
# 实例筛选
418424
def filter_instance_func(_query_params, _cluster_queryset, _proxy_queryset, _storage_queryset):
@@ -495,7 +501,7 @@ def _filter_cluster_hook(
495501
to_attr="storage_set_dtl",
496502
),
497503
Prefetch("clusterentry_set", to_attr="entries"),
498-
"tag_set",
504+
"tags",
499505
)
500506
# 由于对 queryset 切片工作方式的模糊性,这里的values可能会获得非预期的排序,所以不要在切片后用values
501507
# cluster_ids = list(cluster_queryset.values_list("id", flat=True))
@@ -618,6 +624,7 @@ def _to_cluster_representation(
618624
"updater": cluster.updater,
619625
"create_at": datetime2str(cluster.create_at),
620626
"update_at": datetime2str(cluster.update_at),
627+
"tags": [{tag.key: tag.value} for tag in cluster.tags.all()],
621628
}
622629

623630
@classmethod

dbm-ui/backend/db_services/dbbase/resources/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class ListResourceSLZ(serializers.Serializer):
3232
bk_cloud_id = serializers.CharField(required=False, help_text=_("管控区域"))
3333
cluster_type = serializers.CharField(required=False, help_text=_("集群类型"))
3434
ordering = serializers.CharField(required=False, help_text=_("排序字段,非必填"))
35+
tag_ids = serializers.CharField(required=False, help_text=_("标签"))
3536

3637

3738
class ListMySQLResourceSLZ(ListResourceSLZ):
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
4+
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
5+
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at https://opensource.org/licenses/MIT
7+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
specific language governing permissions and limitations under the License.
10+
"""
11+
from django.utils.translation import gettext_lazy as _
12+
13+
from blue_krill.data_types.enum import EnumField, StructuredEnum
14+
15+
16+
class TagResourceType(str, StructuredEnum):
17+
DB_RESOURCE = EnumField("db_resource", _("资源池"))
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
4+
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
5+
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at https://opensource.org/licenses/MIT
7+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
specific language governing permissions and limitations under the License.
10+
"""
11+
from typing import Dict, List
12+
13+
from django.db.models import ManyToManyRel
14+
from django.utils.translation import gettext_lazy as _
15+
16+
from backend.db_meta.models import Tag
17+
from backend.db_services.tag.constants import TagResourceType
18+
from backend.exceptions import ValidationError
19+
20+
21+
class TagHandler:
22+
"""标签的操作类"""
23+
24+
def batch_set_tags(self, tag_ids: List[int]):
25+
"""
26+
给资源批量设置标签
27+
"""
28+
# 1. 判断标签中 key 是否允许多值
29+
30+
# 2. 批量设置标签
31+
pass
32+
33+
@classmethod
34+
def delete_tags(cls, bk_biz_id: int, ids: List[int]):
35+
"""
36+
删除标签
37+
"""
38+
# 1. 检查标签是否被引用
39+
related_resources = cls.query_related_resources(ids)
40+
for related_resource in related_resources:
41+
if related_resource["count"] > 0:
42+
raise ValidationError(_("标签被引用,无法删除"))
43+
44+
# 2. 批量删除标签
45+
Tag.objects.filter(bk_biz_id=bk_biz_id, id__in=ids).delete()
46+
47+
@classmethod
48+
def query_related_resources(cls, ids: List[int], resource_type: str = None):
49+
"""
50+
查询关联资源
51+
"""
52+
# 1. 查询外键关联资源
53+
data = []
54+
for tag_id in ids:
55+
info = {"id": tag_id, "related_resources": []}
56+
for field in Tag._meta.get_fields():
57+
if isinstance(field, ManyToManyRel) and (field.name == resource_type or resource_type is None):
58+
related_objs = field.related_model.objects.prefetch_related("tags").filter(tags__id=tag_id)
59+
info["related_resources"].append(
60+
{
61+
"resource_type": field.name,
62+
"count": related_objs.count(),
63+
}
64+
)
65+
66+
# 2. 查询第三方服务关联资源(如资源池、后续可能扩展的别的服务)
67+
if resource_type == TagResourceType.DB_RESOURCE.value or resource_type is None:
68+
info["related_resources"].append(
69+
{
70+
"resource_type": TagResourceType.DB_RESOURCE.value,
71+
# TODO 请求资源池接口得到统计数量
72+
"count": 0,
73+
}
74+
)
75+
data.append(info)
76+
return data
77+
78+
@classmethod
79+
def batch_create(cls, bk_biz_id: int, tags: List[Dict[str, str]], creator: str = ""):
80+
"""
81+
批量创建标签
82+
"""
83+
duplicate_tags = cls.verify_duplicated(bk_biz_id, tags)
84+
if duplicate_tags:
85+
raise ValidationError(_("检查到重复的标签"), data=duplicate_tags)
86+
87+
tag_models = [Tag(bk_biz_id=bk_biz_id, key=tag["key"], value=tag["value"], creator=creator) for tag in tags]
88+
Tag.objects.bulk_create(tag_models)
89+
90+
@classmethod
91+
def verify_duplicated(cls, bk_biz_id: int, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
92+
"""
93+
检查标签是否重复
94+
"""
95+
biz_tags = [f"{tag.key}:{tag.value}" for tag in Tag.objects.filter(bk_biz_id=bk_biz_id)]
96+
duplicate_tags = []
97+
for tag in tags:
98+
if f'{tag["key"]}:{tag["value"]}' in biz_tags:
99+
duplicate_tags.append(tag)
100+
return duplicate_tags
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-DB管理系统(BlueKing-BK-DBM) available.
4+
Copyright (C) 2017-2023 THL A29 Limited, a Tencent company. All rights reserved.
5+
Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at https://opensource.org/licenses/MIT
7+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9+
specific language governing permissions and limitations under the License.
10+
"""
11+
12+
from django.utils.translation import gettext_lazy as _
13+
from rest_framework import serializers
14+
15+
from backend.bk_web.serializers import AuditedSerializer
16+
from backend.db_meta.models import Tag
17+
18+
19+
class TagSerializer(AuditedSerializer, serializers.ModelSerializer):
20+
"""
21+
标签序列化器
22+
"""
23+
24+
class Meta:
25+
model = Tag
26+
fields = "__all__"
27+
28+
29+
class BatchCreateTagsSerializer(serializers.Serializer):
30+
class CreateTagSerializer(serializers.Serializer):
31+
key = serializers.CharField(help_text=_("标签key"))
32+
value = serializers.CharField(help_text=_("标签value"))
33+
34+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
35+
tags = serializers.ListField(child=CreateTagSerializer())
36+
37+
38+
class UpdateTagSerializer(serializers.Serializer):
39+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
40+
id = serializers.IntegerField(help_text=_("标签 ID"))
41+
value = serializers.CharField(help_text=_("标签value"))
42+
43+
44+
class DeleteTagsSerializer(serializers.Serializer):
45+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
46+
ids = serializers.ListSerializer(child=serializers.IntegerField(help_text=_("标签 ID")), help_text=_("标签 ID 列表"))
47+
48+
49+
class QueryRelatedResourceSerializer(serializers.Serializer):
50+
ids = serializers.ListSerializer(child=serializers.IntegerField(help_text=_("标签 ID")), help_text=_("标签 ID 列表"))
51+
resource_type = serializers.CharField(help_text=_("资源类型"), required=False)

0 commit comments

Comments
 (0)