Skip to content

Commit e577b13

Browse files
committed
feat(backend): 集群标签管理 #10165
# Reviewed, transaction id: 39592
1 parent 51e9db1 commit e577b13

File tree

27 files changed

+1255
-127
lines changed

27 files changed

+1255
-127
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-04-21 03:44
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("db_meta", "0048_auto_20250408_1828"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="tag",
15+
name="is_builtin",
16+
field=models.BooleanField(default=False, help_text="是否内置"),
17+
),
18+
migrations.AlterField(
19+
model_name="clusterdbhaext",
20+
name="begin_time",
21+
field=models.DateTimeField(auto_now_add=True, db_index=True, help_text="屏蔽开始时间"),
22+
),
23+
migrations.AlterField(
24+
model_name="spec",
25+
name="cpu",
26+
field=models.JSONField(default=dict, help_text='cpu规格描述:{"min":1,"max":10}', null=True),
27+
),
28+
migrations.AlterField(
29+
model_name="spec",
30+
name="device_class",
31+
field=models.JSONField(default=dict, help_text='实际机器机型: ["class1","class2"]', null=True),
32+
),
33+
migrations.AlterField(
34+
model_name="spec",
35+
name="mem",
36+
field=models.JSONField(default=dict, help_text='mem规格描述:{"min":100,"max":1000}', null=True),
37+
),
38+
migrations.AlterField(
39+
model_name="spec",
40+
name="qps",
41+
field=models.JSONField(default=dict, help_text='qps规格描述:{"min": 1, "max": 100}', null=True),
42+
),
43+
migrations.AlterField(
44+
model_name="spec",
45+
name="storage_spec",
46+
field=models.JSONField(
47+
default=dict, help_text='存储磁盘需求配置:[{"mount_point":"/data","size":500,"type":"ssd"}]', null=True
48+
),
49+
),
50+
migrations.AlterField(
51+
model_name="tag",
52+
name="type",
53+
field=models.CharField(
54+
choices=[("resource", "资源标签"), ("cluster", "集群标签")], help_text="tag类型", max_length=64
55+
),
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_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
@@ -244,3 +244,21 @@ def validate(self, attrs):
244244
raise serializers.ValidationError(_("The new alias cannot be the same as the current alias."))
245245

246246
return attrs
247+
248+
249+
class UpdateClusterTagsSerializer(serializers.Serializer):
250+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
251+
cluster_id = serializers.IntegerField(help_text=_("集群ID"))
252+
tags = serializers.ListField(child=serializers.IntegerField(), help_text=_("标签列表"))
253+
254+
255+
class RemoveClusterTagKeysSerializer(serializers.Serializer):
256+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
257+
cluster_ids = serializers.ListField(child=serializers.IntegerField(), help_text=_("集群ID列表"))
258+
keys = serializers.ListField(child=serializers.CharField(), help_text=_("标签键列表"))
259+
260+
261+
class AddClusterTagKeysSerializer(serializers.Serializer):
262+
bk_biz_id = serializers.IntegerField(help_text=_("业务ID"))
263+
cluster_ids = serializers.ListField(child=serializers.IntegerField(), help_text=_("集群ID列表"))
264+
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 CheckClusterDbsResponseSerializer, CheckClusterDbsSerializer
3131
from backend.db_services.dbbase.instances.handlers import InstanceHandler
@@ -34,6 +34,7 @@
3434
from backend.db_services.dbbase.resources.query import ListRetrieveResource, ResourceList
3535
from backend.db_services.dbbase.resources.serializers import ClusterSLZ
3636
from backend.db_services.dbbase.serializers import (
37+
AddClusterTagKeysSerializer,
3738
ClusterDbTypeSerializer,
3839
ClusterEntryFilterSerializer,
3940
ClusterFilterSerializer,
@@ -49,16 +50,22 @@
4950
QueryClusterCapResponseSerializer,
5051
QueryClusterCapSerializer,
5152
QueryClusterInstanceCountSerializer,
53+
RemoveClusterTagKeysSerializer,
5254
ResourceAdministrationSerializer,
5355
UpdateClusterAliasSerializer,
56+
UpdateClusterTagsSerializer,
5457
WebConsoleResponseSerializer,
5558
WebConsoleSerializer,
5659
)
5760
from backend.db_services.ipchooser.query.resource import ResourceQueryHelper
5861
from backend.db_services.mysql.remote_service.handlers import RemoteServiceHandler
5962
from backend.db_services.redis.toolbox.handlers import ToolboxHandler
6063
from backend.iam_app.handlers.drf_perm.base import DBManagePermission
61-
from backend.iam_app.handlers.drf_perm.cluster import ClusterDBConsolePermission, ClusterWebconsolePermission
64+
from backend.iam_app.handlers.drf_perm.cluster import (
65+
ClusterDBConsolePermission,
66+
ClusterEditPermission,
67+
ClusterWebconsolePermission,
68+
)
6269

6370
SWAGGER_TAG = _("集群通用接口")
6471

@@ -78,9 +85,16 @@ class DBBaseViewSet(viewsets.SystemViewSet):
7885
(
7986
"simple_query_cluster",
8087
"common_query_cluster",
88+
"filter_clusters",
8189
): [DBManagePermission()],
8290
("webconsole",): [ClusterWebconsolePermission()],
8391
("dbconsole",): [ClusterDBConsolePermission()],
92+
(
93+
"update_cluster_alias",
94+
"update_cluster_tag",
95+
"remove_cluster_tag_keys",
96+
"add_cluster_tag_keys",
97+
): [ClusterEditPermission()],
8498
}
8599
default_permission_class = [DBManagePermission()]
86100

@@ -452,5 +466,54 @@ def update_cluster_alias(self, request):
452466
cluster = Cluster.objects.get(bk_biz_id=validated_data["bk_biz_id"], id=validated_data["cluster_id"])
453467
cluster.alias = validated_data["new_alias"]
454468
cluster.save(update_fields=["alias"])
455-
serializer = ClusterSLZ(cluster)
456-
return Response(serializer.data)
469+
return Response(ClusterSLZ(cluster).data)
470+
471+
@common_swagger_auto_schema(
472+
operation_summary=_("更新集群标签"),
473+
request_body=UpdateClusterTagsSerializer(),
474+
tags=[SWAGGER_TAG],
475+
)
476+
@action(methods=["POST"], detail=False, serializer_class=UpdateClusterTagsSerializer)
477+
def update_cluster_tag(self, request):
478+
"""更新集群标签"""
479+
data = self.params_validate(self.get_serializer_class())
480+
cluster = Cluster.objects.get(bk_biz_id=data["bk_biz_id"], id=data["cluster_id"])
481+
tags = Tag.objects.filter(id__in=data["tags"])
482+
# 清空旧标签,添加新标签
483+
cluster.tags.clear()
484+
cluster.tags.add(*tags)
485+
return Response(ClusterSLZ(cluster).data)
486+
487+
@common_swagger_auto_schema(
488+
operation_summary=_("批量移除标签键"),
489+
request_body=RemoveClusterTagKeysSerializer(),
490+
tags=[SWAGGER_TAG],
491+
)
492+
@action(methods=["POST"], detail=False, serializer_class=RemoveClusterTagKeysSerializer)
493+
def remove_cluster_tag_keys(self, request):
494+
"""批量移除标签键"""
495+
data = self.params_validate(self.get_serializer_class())
496+
tag_ids = list(Tag.objects.filter(key__in=data["keys"]).values_list("id", flat=True))
497+
Cluster.tags.through.objects.filter(cluster_id__in=data["cluster_ids"], tag_id__in=tag_ids).delete()
498+
return Response()
499+
500+
@common_swagger_auto_schema(
501+
operation_summary=_("批量增加标签键"),
502+
request_body=AddClusterTagKeysSerializer(),
503+
tags=[SWAGGER_TAG],
504+
)
505+
@action(methods=["POST"], detail=False, serializer_class=AddClusterTagKeysSerializer)
506+
def add_cluster_tag_keys(self, request):
507+
"""批量增加标签键"""
508+
data = self.params_validate(self.get_serializer_class())
509+
510+
tags = Tag.objects.filter(id__in=data["tags"])
511+
through = Cluster.tags.through
512+
513+
# 这里需要先获取到所有标签键,然后排除已经存在该key的集群,考虑到这里标签一次性不会太多,暂用for循环处理
514+
for tag in tags:
515+
add_clusters = Cluster.objects.filter(id__in=data["cluster_ids"]).exclude(tags__key=tag.key)
516+
add_tags = [through(cluster_id=cluster.id, tag_id=tag.id) for cluster in add_clusters]
517+
through.objects.bulk_create(add_tags)
518+
519+
return Response()

dbm-ui/backend/db_services/mongodb/resources/views.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ class MongoDBViewSet(ResourceViewSet):
101101
query_serializer_class = serializers.ListMongoDBResourceSLZ
102102
list_instances_slz = serializers.MongoDBListInstancesSerializer
103103

104-
list_perm_actions = [ActionEnum.MONGODB_VIEW, ActionEnum.MONGODB_ENABLE_DISABLE, ActionEnum.MONGODB_DESTROY]
104+
list_perm_actions = [
105+
ActionEnum.MONGODB_VIEW,
106+
ActionEnum.MONGODB_EDIT,
107+
ActionEnum.MONGODB_ENABLE_DISABLE,
108+
ActionEnum.MONGODB_DESTROY,
109+
]
105110
list_instance_perm_actions = [ActionEnum.MONGODB_VIEW]
106111

107112
@common_swagger_auto_schema(

dbm-ui/backend/db_services/mysql/resources/tendbcluster/views.py

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class SpiderViewSet(viewsets.ResourceViewSet):
9494

9595
list_perm_actions = [
9696
ActionEnum.TENDBCLUSTER_VIEW,
97+
ActionEnum.TENDBCLUSTER_EDIT,
9798
ActionEnum.TENDBCLUSTER_SPIDER_SLAVE_DESTROY,
9899
ActionEnum.TENDBCLUSTER_ENABLE_DISABLE,
99100
ActionEnum.TENDBCLUSTER_WEBCONSOLE,

0 commit comments

Comments
 (0)