Skip to content

Commit f49794f

Browse files
committed
feat(backend): 集群标签管理 #10165
# Reviewed, transaction id: 41685
1 parent 26890a2 commit f49794f

File tree

27 files changed

+448
-69
lines changed

27 files changed

+448
-69
lines changed

dbm-ui/backend/configuration/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ class SystemSettingsEnum(str, StructuredEnum):
134134
MACHINE_PROPERTY = EnumField("MACHINE_PROPERTY", _("主机属性开关"))
135135
PADDING_PROXY_APPS = EnumField("PADDING_PROXY_APPS", _("补全proxy业务"))
136136
DISABLE_DBHA_APPS_CLUSTER_TYPE = EnumField("DISABLE_DBHA_APPS_CLUSTER_TYPE", _("禁用DBHA业务"))
137+
# 内置标签列表
138+
BUILTIN_LABELS = EnumField("BUILTIN_LABELS", _("内置标签列表"))
137139

138140

139141
class BizSettingsEnum(str, StructuredEnum):

dbm-ui/backend/configuration/views/system.py

+8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ class SystemSettingsViewSet(viewsets.SystemViewSet):
5050
}
5151
default_permission_class = [ResourceActionPermission([ActionEnum.GLOBAL_MANAGE])]
5252

53+
@common_swagger_auto_schema(
54+
operation_summary=_("查询内置标签"),
55+
tags=tags,
56+
)
57+
@action(methods=["GET"], detail=False)
58+
def builtin_labels(self, request, *args, **kwargs):
59+
return Response(SystemSettings.get_setting_value(SystemSettingsEnum.BUILTIN_LABELS.value, default=[]))
60+
5361
@common_swagger_auto_schema(
5462
operation_summary=_("查询磁盘类型"),
5563
tags=tags,

dbm-ui/backend/db_meta/admin.py

+6
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,9 @@ class ClusterMonitorTopoAdmin(admin.ModelAdmin):
213213
class SyncFailedMachineAdmin(admin.ModelAdmin):
214214
list_display = ("bk_host_id", "error")
215215
search_fields = ("error",)
216+
217+
218+
@admin.register(models.tag.Tag)
219+
class TagAdmin(admin.ModelAdmin):
220+
list_display = ("key", "value", "bk_biz_id", "is_builtin", "type")
221+
search_fields = ("bk_biz_id", "key", "value")

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

+11-11
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,21 @@ class DBCCModule(str, StructuredEnum):
2525
MONGODB = EnumField("mongodb", _("mongodb"))
2626

2727

28+
class RedisVerUpdateNodeType(str, StructuredEnum):
29+
"""redis版本升级节点类型"""
30+
31+
Proxy = EnumField("Proxy", _("Proxy"))
32+
Backend = EnumField("Backend", _("Backend"))
33+
34+
2835
class TagType(str, StructuredEnum):
29-
CUSTOM = EnumField("custom", _("自定义标签"))
30-
SYSTEM = EnumField("system", _("系统标签"))
31-
BUILTIN = EnumField("builtin", _("内置标签"))
36+
"""标签类型"""
37+
38+
RESOURCE = EnumField("resource", _("资源标签"))
39+
CLUSTER = EnumField("cluster", _("集群标签"))
3240

3341

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

3745
TEMPORARY = EnumField("temporary", _("临时集群"))
38-
RESOURCE_TAG = EnumField("resource", _("资源标签"))
39-
40-
41-
class RedisVerUpdateNodeType(str, StructuredEnum):
42-
"""redis版本升级节点类型"""
43-
44-
Proxy = EnumField("Proxy", _("Proxy"))
45-
Backend = EnumField("Backend", _("Backend"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Generated by Django 3.2.25 on 2025-05-08 13:42
2+
3+
from django.db import migrations, models
4+
5+
from backend.db_meta.enums.comm import TagType
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("db_meta", "0049_auto_20250507_1626"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="tag",
17+
name="is_builtin",
18+
field=models.BooleanField(default=False, help_text="是否内置"),
19+
),
20+
migrations.AlterField(
21+
model_name="clusterdbhaext",
22+
name="begin_time",
23+
field=models.DateTimeField(auto_now_add=True, db_index=True, help_text="屏蔽开始时间"),
24+
),
25+
migrations.AlterField(
26+
model_name="spec",
27+
name="cpu",
28+
field=models.JSONField(default=dict, help_text='cpu规格描述:{"min":1,"max":10}', null=True),
29+
),
30+
migrations.AlterField(
31+
model_name="spec",
32+
name="device_class",
33+
field=models.JSONField(default=dict, help_text='实际机器机型: ["class1","class2"]', null=True),
34+
),
35+
migrations.AlterField(
36+
model_name="spec",
37+
name="mem",
38+
field=models.JSONField(default=dict, help_text='mem规格描述:{"min":100,"max":1000}', null=True),
39+
),
40+
migrations.AlterField(
41+
model_name="spec",
42+
name="qps",
43+
field=models.JSONField(default=dict, help_text='qps规格描述:{"min": 1, "max": 100}', null=True),
44+
),
45+
migrations.AlterField(
46+
model_name="spec",
47+
name="storage_spec",
48+
field=models.JSONField(
49+
default=dict, help_text='存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]', null=True
50+
),
51+
),
52+
migrations.AlterField(
53+
model_name="tag",
54+
name="type",
55+
field=models.CharField(choices=TagType.get_choices(), help_text="tag类型", max_length=64),
56+
),
57+
]

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,18 @@ class Spec(AuditedModel):
3737
spec_name = models.CharField(max_length=128, help_text=_("虚拟规格名称"))
3838
spec_cluster_type = models.CharField(max_length=64, choices=SpecClusterType.get_choices(), help_text=_("组件类型"))
3939
spec_machine_type = models.CharField(max_length=64, choices=SpecMachineType.get_choices(), help_text=_("机器类型"))
40-
cpu = models.JSONField(help_text=_('cpu规格描述:{"min":1,"max":10}'), default=dict)
41-
mem = models.JSONField(help_text=_('mem规格描述:{"min":100,"max":1000}'), default=dict)
42-
device_class = models.JSONField(help_text=_('实际机器机型: ["class1","class2"]'), default=dict)
40+
cpu = models.JSONField(null=True, help_text=_('cpu规格描述:{"min":1,"max":10}'), default=dict)
41+
mem = models.JSONField(null=True, help_text=_('mem规格描述:{"min":100,"max":1000}'), default=dict)
42+
device_class = models.JSONField(null=True, help_text=_('实际机器机型: ["class1","class2"]'), default=dict)
4343
storage_spec = models.JSONField(
44-
help_text=_('存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]'), default=dict
44+
help_text=_('存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]'), default=dict, null=True
4545
)
4646
desc = models.TextField(help_text=_("资源规格描述"), null=True, blank=True)
4747
enable = models.BooleanField(help_text=_("是否启用"), default=True)
4848
# es专属
4949
instance_num = models.IntegerField(default=0, help_text=_("实例数(es专属)"))
5050
# spider,redis集群专属
51-
qps = models.JSONField(default=dict, help_text=_('qps规格描述:{"min": 1, "max": 100}'))
51+
qps = models.JSONField(default=dict, help_text=_('qps规格描述:{"min": 1, "max": 100}'), null=True)
5252

5353
class Meta:
5454
verbose_name = verbose_name_plural = _("资源规格(Spec)")

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

+13-9
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,25 @@ class Tag(AuditedModel):
2020
bk_biz_id = models.IntegerField(help_text=_("业务 ID"), default=0)
2121
key = models.CharField(help_text=_("标签键"), default="", max_length=64)
2222
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-
)
23+
24+
type = models.CharField(help_text=_("tag类型"), max_length=64, choices=TagType.get_choices())
25+
is_builtin = models.BooleanField(help_text=_("是否内置"), default=False)
2626

2727
class Meta:
2828
unique_together = ["bk_biz_id", "key", "value"]
2929

3030
@property
31-
def tag_desc(self):
32-
"""仅返回tag的信息"""
33-
return {"bk_biz_id": self.bk_biz_id, "key": self.key, "type": self.type}
31+
def desc(self):
32+
return {"key": self.key, "value": self.value, "is_builtin": self.is_builtin, "id": self.id}
3433

3534
@classmethod
36-
def get_or_create_system_tag(cls, key: str, value: str):
35+
def get_builtin_tag(cls, key, value, type):
36+
"""获取内置tag,如果不存在则创建"""
3737
tag, created = cls.objects.get_or_create(
38-
bk_biz_id=PLAT_BIZ_ID, key=key, value=value, type=TagType.SYSTEM.value
38+
key=key,
39+
value=value,
40+
type=type,
41+
is_builtin=True,
42+
defaults={"bk_biz_id": PLAT_BIZ_ID},
3943
)
40-
return tag
44+
return tag, created

dbm-ui/backend/db_proxy/migrations/0001_initial.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from django.db import migrations, models
44

55
from backend.configuration.constants import DBType
6-
from backend.db_meta.enums import InstanceStatus
7-
from backend.db_proxy.constants import ExtensionType
6+
from backend.db_proxy.constants import ExtensionServiceStatus, ExtensionType
87

98

109
class Migration(migrations.Migration):
@@ -50,7 +49,7 @@ class Migration(migrations.Migration):
5049
(
5150
"status",
5251
models.CharField(
53-
choices=InstanceStatus.get_choices(),
52+
choices=ExtensionServiceStatus.get_choices(),
5453
max_length=64,
5554
verbose_name="服务状态",
5655
),

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

+11-10
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,9 @@ def _list_clusters(
430430
# 域名
431431
"domain": build_q_for_domain_by_cluster(domains=query_params.get("domain", "").split(",")),
432432
# 标签
433-
"tags": Q(tags__in=query_params.get("tags", "").split(",")),
433+
"tag_ids": Q(tags__in=query_params.get("tag_ids", "").split(",")),
434+
# 标签key过滤
435+
"tag_keys": Q(tags__key__in=query_params.get("tag_keys", "").split(",")),
434436
}
435437

436438
filter_params_map.update(inner_filter_params_map)
@@ -590,21 +592,19 @@ def _to_cluster_representation(
590592
@param cluster_entry_map: key 是 cluster.id, value 是当前集群对应的 entry 映射
591593
@param cluster_operate_records_map: key 是 cluster.id, value 是当前集群对应的 操作记录 映射
592594
"""
593-
spec = None
595+
cluster_spec = None
594596
cluster_entry_map_value = ClusterEntry.get_entries_map(entries=cluster.entries).get(cluster.id, {})
595597
bk_cloud_name = cloud_info.get(str(cluster.bk_cloud_id), {}).get("bk_cloud_name", "")
596598

597599
# 补充集群规格信息
598-
if cls.storage_spec_role is not None:
599-
storage_spec = next(
600-
(storage for storage in cluster.storages if storage.instance_role == cls.storage_spec_role), None
601-
)
602-
if storage_spec:
603-
spec_id = storage_spec.machine.spec_id
604-
spec = kwargs["remote_spec_map"].get(spec_id)
600+
if cls.storage_spec_role:
601+
storage = next((inst for inst in cluster.storages if inst.instance_role == cls.storage_spec_role), None)
602+
cluster_spec_id = storage.machine.spec_id if storage else 0
603+
cluster_spec = kwargs["remote_spec_map"].get(cluster_spec_id)
605604

606605
return {
607606
"id": cluster.id,
607+
"db_type": ClusterType.cluster_type_to_db_type(cluster.cluster_type),
608608
"phase": cluster.phase,
609609
"phase_name": cluster.get_phase_display(),
610610
"status": cluster.status,
@@ -633,7 +633,8 @@ def _to_cluster_representation(
633633
"updater": cluster.updater,
634634
"create_at": datetime2str(cluster.create_at),
635635
"update_at": datetime2str(cluster.update_at),
636-
"cluster_spec": model_to_dict(spec) if spec else None,
636+
"cluster_spec": model_to_dict(cluster_spec) if cluster_spec else None,
637+
"tags": [tag.desc for tag in cluster.tags.all()],
637638
}
638639

639640
@classmethod

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

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ListResourceSLZ(serializers.Serializer):
4242
cluster_type = serializers.CharField(required=False, help_text=_("集群类型"))
4343
ordering = serializers.CharField(required=False, help_text=_("排序字段,非必填"))
4444
tag_ids = serializers.CharField(required=False, help_text=_("标签"))
45+
tag_keys = serializers.CharField(required=False, help_text=_("标签键"))
4546

4647

4748
class ListMySQLResourceSLZ(ListResourceSLZ):

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

+18
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,21 @@ def validate(self, attrs):
247247
raise serializers.ValidationError(_("The new alias cannot be the same as the current alias."))
248248

249249
return attrs
250+
251+
252+
class UpdateClusterTagsSerializer(serializers.Serializer):
253+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
254+
cluster_id = serializers.IntegerField(help_text=_("集群ID"))
255+
tags = serializers.ListField(child=serializers.IntegerField(), help_text=_("标签列表"))
256+
257+
258+
class RemoveClusterTagKeysSerializer(serializers.Serializer):
259+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
260+
cluster_ids = serializers.ListField(child=serializers.IntegerField(), help_text=_("集群ID列表"))
261+
keys = serializers.ListField(child=serializers.CharField(), help_text=_("标签键列表"))
262+
263+
264+
class AddClusterTagKeysSerializer(serializers.Serializer):
265+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
266+
cluster_ids = serializers.ListField(child=serializers.IntegerField(), help_text=_("集群ID列表"))
267+
tags = serializers.ListField(child=serializers.IntegerField(), help_text=_("标签列表"))

dbm-ui/backend/db_services/dbbase/views.py

+67-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from backend.components import BKBaseApi
2626
from backend.configuration.constants import DBType
2727
from backend.db_meta.enums import ClusterType, InstanceRole
28-
from backend.db_meta.models import Cluster, DBModule, ProxyInstance, StorageInstance
28+
from backend.db_meta.models import Cluster, DBModule, ProxyInstance, StorageInstance, Tag
2929
from backend.db_services.dbbase.cluster.handlers import ClusterServiceHandler
3030
from backend.db_services.dbbase.cluster.serializers import (
3131
BatchCheckClusterDbsSerializer,
@@ -38,6 +38,7 @@
3838
from backend.db_services.dbbase.resources.query import ListRetrieveResource, ResourceList
3939
from backend.db_services.dbbase.resources.serializers import ClusterSLZ
4040
from backend.db_services.dbbase.serializers import (
41+
AddClusterTagKeysSerializer,
4142
ClusterDbTypeSerializer,
4243
ClusterEntryFilterSerializer,
4344
ClusterFilterSerializer,
@@ -53,8 +54,10 @@
5354
QueryClusterCapResponseSerializer,
5455
QueryClusterCapSerializer,
5556
QueryClusterInstanceCountSerializer,
57+
RemoveClusterTagKeysSerializer,
5658
ResourceAdministrationSerializer,
5759
UpdateClusterAliasSerializer,
60+
UpdateClusterTagsSerializer,
5861
WebConsoleResponseSerializer,
5962
WebConsoleSerializer,
6063
)
@@ -63,7 +66,11 @@
6366
from backend.db_services.mysql.remote_service.handlers import RemoteServiceHandler
6467
from backend.db_services.redis.toolbox.handlers import ToolboxHandler
6568
from backend.iam_app.handlers.drf_perm.base import DBManagePermission
66-
from backend.iam_app.handlers.drf_perm.cluster import ClusterDBConsolePermission, ClusterWebconsolePermission
69+
from backend.iam_app.handlers.drf_perm.cluster import (
70+
ClusterDBConsolePermission,
71+
ClusterEditPermission,
72+
ClusterWebconsolePermission,
73+
)
6774

6875
SWAGGER_TAG = _("集群通用接口")
6976

@@ -83,9 +90,16 @@ class DBBaseViewSet(viewsets.SystemViewSet):
8390
(
8491
"simple_query_cluster",
8592
"common_query_cluster",
93+
"filter_clusters",
8694
): [DBManagePermission()],
8795
("webconsole",): [ClusterWebconsolePermission()],
8896
("dbconsole",): [ClusterDBConsolePermission()],
97+
(
98+
"update_cluster_alias",
99+
"update_cluster_tag",
100+
"remove_cluster_tag_keys",
101+
"add_cluster_tag_keys",
102+
): [ClusterEditPermission()],
89103
}
90104
default_permission_class = [DBManagePermission()]
91105

@@ -479,5 +493,54 @@ def update_cluster_alias(self, request):
479493
cluster = Cluster.objects.get(bk_biz_id=validated_data["bk_biz_id"], id=validated_data["cluster_id"])
480494
cluster.alias = validated_data["new_alias"]
481495
cluster.save(update_fields=["alias"])
482-
serializer = ClusterSLZ(cluster)
483-
return Response(serializer.data)
496+
return Response(ClusterSLZ(cluster).data)
497+
498+
@common_swagger_auto_schema(
499+
operation_summary=_("更新集群标签"),
500+
request_body=UpdateClusterTagsSerializer(),
501+
tags=[SWAGGER_TAG],
502+
)
503+
@action(methods=["POST"], detail=False, serializer_class=UpdateClusterTagsSerializer)
504+
def update_cluster_tag(self, request):
505+
"""更新集群标签"""
506+
data = self.params_validate(self.get_serializer_class())
507+
cluster = Cluster.objects.get(bk_biz_id=data["bk_biz_id"], id=data["cluster_id"])
508+
tags = Tag.objects.filter(id__in=data["tags"])
509+
# 清空旧标签,添加新标签
510+
cluster.tags.clear()
511+
cluster.tags.add(*tags)
512+
return Response(ClusterSLZ(cluster).data)
513+
514+
@common_swagger_auto_schema(
515+
operation_summary=_("批量移除标签键"),
516+
request_body=RemoveClusterTagKeysSerializer(),
517+
tags=[SWAGGER_TAG],
518+
)
519+
@action(methods=["POST"], detail=False, serializer_class=RemoveClusterTagKeysSerializer)
520+
def remove_cluster_tag_keys(self, request):
521+
"""批量移除标签键"""
522+
data = self.params_validate(self.get_serializer_class())
523+
tag_ids = list(Tag.objects.filter(key__in=data["keys"]).values_list("id", flat=True))
524+
Cluster.tags.through.objects.filter(cluster_id__in=data["cluster_ids"], tag_id__in=tag_ids).delete()
525+
return Response()
526+
527+
@common_swagger_auto_schema(
528+
operation_summary=_("批量增加标签键"),
529+
request_body=AddClusterTagKeysSerializer(),
530+
tags=[SWAGGER_TAG],
531+
)
532+
@action(methods=["POST"], detail=False, serializer_class=AddClusterTagKeysSerializer)
533+
def add_cluster_tag_keys(self, request):
534+
"""批量增加标签键"""
535+
data = self.params_validate(self.get_serializer_class())
536+
537+
tags = Tag.objects.filter(id__in=data["tags"])
538+
through = Cluster.tags.through
539+
540+
# 这里需要先获取到所有标签键,然后排除已经存在该key的集群,考虑到这里标签一次性不会太多,暂用for循环处理
541+
for tag in tags:
542+
add_clusters = Cluster.objects.filter(id__in=data["cluster_ids"]).exclude(tags__key=tag.key)
543+
add_tags = [through(cluster_id=cluster.id, tag_id=tag.id) for cluster in add_clusters]
544+
through.objects.bulk_create(add_tags)
545+
546+
return Response()

0 commit comments

Comments
 (0)