From c342ccef68e2bcad6bda522c3cfbbc5b3dc65777 Mon Sep 17 00:00:00 2001 From: GONGONGONG <506419689@qq.com> Date: Fri, 26 Jan 2024 11:16:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=E7=82=B9=E6=94=B9?= =?UTF-8?q?=E9=80=A0(closed=20#852)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- .../test_check_policy_gse_to_proxy.py | 4 +- apps/mock_data/common_unit/host.py | 15 +- .../management/commands/transform_ap_data.py | 86 ++ .../0082_covert_ap_data_20231109_1336.py | 26 + apps/node_man/models.py | 54 +- .../periodic_tasks/gse_svr_discovery.py | 19 +- apps/node_man/serializers/ap.py | 55 +- .../tests/test_pericdic_tasks/mock_data.py | 17 +- .../test_gse_svr_discovery.py | 26 +- .../tests/test_tools/test_ap_transform.py | 639 +++++++++++++ apps/node_man/tests/utils.py | 10 +- apps/node_man/utils/endpoint.py | 127 ++- apps/node_man/views/ap.py | 72 +- frontend/.eslintrc.js | 1 + frontend/src/common/form-check.ts | 29 +- frontend/src/common/form-label-hook.ts | 34 + frontend/src/common/regexp.ts | 2 +- .../src/components/RussianDolls/DollForm.vue | 20 +- .../RussianDolls/item/DollArray.vue | 20 +- .../components/RussianDolls/item/DollBase.vue | 30 +- .../RussianDolls/item/DollIndex.vue | 25 +- .../RussianDolls/item/DollObject.vue | 25 +- .../src/components/common/strategy-table.vue | 4 +- .../components/common/strategy-template.vue | 8 +- frontend/src/config/test-anchor-key.ts | 1 + frontend/src/i18n/en.js | 14 +- frontend/src/i18n/zh.js | 20 +- frontend/src/setup/i18n-setup.ts | 2 + frontend/src/store/modules/config.ts | 34 +- frontend/src/types/config/config.ts | 43 +- .../cloud/cloud-channel/channel-edit.vue | 6 +- .../cloud/cloud-channel/channel-table.vue | 8 +- .../gse-config/access-point-table.vue | 243 +++-- .../views/global-config/gse-config/index.vue | 7 +- .../set-access-point/apFormConfig.ts | 88 +- .../set-access-point/host-td-input.vue | 70 ++ .../set-access-point/host-td-operate.vue | 61 ++ .../set-access-point/step-form-table.vue | 2 +- .../gse-config/set-access-point/step-host.vue | 905 ++++++++++-------- .../gse-config/set-access-point/step-info.vue | 19 +- 41 files changed, 2148 insertions(+), 729 deletions(-) create mode 100644 apps/node_man/management/commands/transform_ap_data.py create mode 100644 apps/node_man/migrations/0082_covert_ap_data_20231109_1336.py create mode 100644 apps/node_man/tests/test_tools/test_ap_transform.py create mode 100644 frontend/src/common/form-label-hook.ts create mode 100644 frontend/src/views/global-config/gse-config/set-access-point/host-td-input.vue create mode 100644 frontend/src/views/global-config/gse-config/set-access-point/host-td-operate.vue diff --git a/.gitignore b/.gitignore index cd5b58fe3..936953f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -188,4 +188,8 @@ build.yml ## Helm support-files/**/private_values.yaml support-files/**/*.tgz -.codecc \ No newline at end of file +.codecc +.idea +.vscode + +pre-*-bkcodeai diff --git a/apps/backend/tests/components/collections/agent_new/test_check_policy_gse_to_proxy.py b/apps/backend/tests/components/collections/agent_new/test_check_policy_gse_to_proxy.py index 7ce0d8d09..cea8e1d2d 100644 --- a/apps/backend/tests/components/collections/agent_new/test_check_policy_gse_to_proxy.py +++ b/apps/backend/tests/components/collections/agent_new/test_check_policy_gse_to_proxy.py @@ -37,7 +37,9 @@ def get_default_case_name(cls) -> str: def setUpTestData(cls): super().setUpTestData() host = models.Host.objects.all()[0] - models.AccessPoint.objects.all().update(btfileserver=[{"inner_ip": host.inner_ip, "outer_ip": host.outer_ip}]) + models.AccessPoint.objects.all().update( + btfileserver={"inner_ip_infos": [{"ip": host.inner_ip}], "outer_ip_infos": [{"ip": host.outer_ip}]} + ) host_data = copy.deepcopy(common_unit.host.HOST_MODEL_DATA) host_data.update({"bk_host_id": cls.obj_factory.RANDOM_BEGIN_HOST_ID + cls.obj_factory.total_host_num}) cls.obj_factory.bulk_create_model(models.Host, [host_data]) diff --git a/apps/mock_data/common_unit/host.py b/apps/mock_data/common_unit/host.py index 33411c20f..80341ca0b 100644 --- a/apps/mock_data/common_unit/host.py +++ b/apps/mock_data/common_unit/host.py @@ -30,9 +30,18 @@ "ap_type": "system", "region_id": "test", "city_id": "test", - "btfileserver": [{"inner_ip": DEFAULT_IP, "outer_ip": DEFAULT_IP}], - "dataserver": [{"inner_ip": DEFAULT_IP, "outer_ip": DEFAULT_IP}], - "taskserver": [{"inner_ip": DEFAULT_IP, "outer_ip": DEFAULT_IP}], + "btfileserver": { + "inner_ip_infos": [{"ip": DEFAULT_IP}], + "outer_ip_infos": [{"ip": DEFAULT_IP}], + }, + "dataserver": { + "inner_ip_infos": [{"ip": DEFAULT_IP}], + "outer_ip_infos": [{"ip": DEFAULT_IP}], + }, + "taskserver": { + "inner_ip_infos": [{"ip": DEFAULT_IP}], + "outer_ip_infos": [{"ip": DEFAULT_IP}], + }, "zk_hosts": [{"zk_ip": DEFAULT_IP, "zk_port": "2181"}], "zk_account": "zk_account", "zk_password": "zk_password", diff --git a/apps/node_man/management/commands/transform_ap_data.py b/apps/node_man/management/commands/transform_ap_data.py new file mode 100644 index 000000000..dc2d78561 --- /dev/null +++ b/apps/node_man/management/commands/transform_ap_data.py @@ -0,0 +1,86 @@ +# coding: utf-8 +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-节点管理(BlueKing-BK-NODEMAN) available. +Copyright (C) 2017-2022 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. +""" +import typing + +from django.core.management.base import BaseCommand, CommandError + +from apps.node_man import models +from apps.node_man.utils.endpoint import EndPointTransform +from common.log import logger + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + "-e", + "--transform", + required=False, + help="AP_ID create from V1 AP_ID", + default=False, + action="store_true", + ) + parser.add_argument( + "-l", + "--transform_endpoint_to_legacy", + action="store_true", + default=False, + help="Clean up the original mapping ID", + ) + parser.add_argument( + "-a", + "--all_ap", + action="store_true", + default=False, + help="Transform all the AP_IDs in the database", + ) + parser.add_argument( + "-t", + "--transform_ap_id", + required=False, + help="Transform target AP_ID in the database", + ) + + def handle(self, **options): + transform_endpoint_to_legacy = options.get("transform_endpoint_to_legacy") + transform = options.get("transform") + if not transform_endpoint_to_legacy and not transform: + raise CommandError("Please specify the AP_ID to be transformed") + if transform and transform_endpoint_to_legacy: + raise CommandError("Please specify only one AP_ID to be transformed") + + all_ap_transform = options.get("all_ap") + transform_ap_id = options.get("transform_ap_id") + if all_ap_transform and transform_ap_id: + raise CommandError("Please specify only one AP_ID to be transformed") + if not all_ap_transform and not transform_ap_id: + raise CommandError("Please specify the AP_ID to be transformed") + + if all_ap_transform: + ap_objects: typing.List[models.AccessPoint] = models.AccessPoint.objects.all() + else: + ap_objects: typing.List[models.AccessPoint] = models.AccessPoint.objects.filter(id=transform_ap_id) + + if transform_endpoint_to_legacy: + transform_func: typing.Callable = EndPointTransform().transform_endpoint_to_legacy + elif transform: + transform_func: typing.Callable = EndPointTransform().transform + else: + raise CommandError("Please specify the transformation method") + + for ap_object in ap_objects: + logger.info(f"Transforming AP_ID: {ap_object.id}") + try: + ap_object.taskserver = transform_func(ap_object.taskserver) + ap_object.dataserver = transform_func(ap_object.dataserver) + ap_object.btfileserver = transform_func(ap_object.btfileserver) + ap_object.save() + except Exception as e: + raise CommandError(f"Failed to transform AP_ID: {ap_object.id}, error: {e}") diff --git a/apps/node_man/migrations/0082_covert_ap_data_20231109_1336.py b/apps/node_man/migrations/0082_covert_ap_data_20231109_1336.py new file mode 100644 index 000000000..c800668a5 --- /dev/null +++ b/apps/node_man/migrations/0082_covert_ap_data_20231109_1336.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.4 on 2023-10-29 05:36 + +from django.db import migrations, models + +from apps.node_man.utils.endpoint import EndPointTransform + + +def covert_ap_data(apps, schema_editor): + AccessPoint = apps.get_model("node_man", "AccessPoint") + aps = AccessPoint.objects.all() + for ap in aps: + # 转换 gse 地址,从一对一关系,转换为两个列表 + ap.btfileserver = EndPointTransform().transform(legacy_endpoints=ap.btfileserver) + ap.dataserver = EndPointTransform().transform(legacy_endpoints=ap.dataserver) + ap.taskserver = EndPointTransform().transform(legacy_endpoints=ap.taskserver) + ap.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("node_man", "0081_auto_20240307_1656"), + ] + + operations = [ + migrations.RunPython(covert_ap_data), + ] diff --git a/apps/node_man/models.py b/apps/node_man/models.py index 7c9a84ddb..0499ae06e 100644 --- a/apps/node_man/models.py +++ b/apps/node_man/models.py @@ -567,15 +567,21 @@ class AccessPoint(models.Model): @property def file_endpoint_info(self) -> EndpointInfo: - return EndpointInfo(inner_server_infos=self.btfileserver, outer_server_infos=self.btfileserver) + return EndpointInfo( + inner_ip_infos=self.btfileserver["inner_ip_infos"], outer_ip_infos=self.btfileserver["outer_ip_infos"] + ) @property def data_endpoint_info(self) -> EndpointInfo: - return EndpointInfo(inner_server_infos=self.dataserver, outer_server_infos=self.dataserver) + return EndpointInfo( + inner_ip_infos=self.dataserver["inner_ip_infos"], outer_ip_infos=self.dataserver["outer_ip_infos"] + ) @property def cluster_endpoint_info(self) -> EndpointInfo: - return EndpointInfo(inner_server_infos=self.taskserver, outer_server_infos=self.taskserver) + return EndpointInfo( + inner_ip_infos=self.taskserver["inner_ip_infos"], outer_ip_infos=self.taskserver["outer_ip_infos"] + ) @classmethod def ap_id_obj_map(cls): @@ -640,12 +646,18 @@ def test(cls, params: dict): 接入点可用性测试 :param params: Dict { - "servers": [ - { - "inner_ip": "127.0.0.1", - "outer_ip": "127.0.0.2" - } - ], + "btfileserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, + "taskserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, + "dataserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, "package_inner_url": "http://127.0.0.1/download/", "package_outer_url": "http://127.0.0.2/download/" } @@ -710,9 +722,10 @@ def _check_callback_url(url: str, _logs: list): test_logs = [] detect_hosts: Set[str] = set() - for server in params.get("btfileserver", []) + params.get("dataserver", []) + params.get("taskserver", []): - detect_hosts.add(server.get("inner_ip") or server.get("inner_ipv6")) + for server_type in ["btfileserver", "dataserver", "taskserver"]: + for ip_info in params[server_type]["inner_ip_infos"]: + detect_hosts.add(ip_info.get("ip")) with ThreadPoolExecutor(max_workers=settings.CONCURRENT_NUMBER) as ex: tasks = [ex.submit(_check_ip, detect_host, test_logs) for detect_host in detect_hosts] tasks.append(ex.submit(_check_package_url, params["package_inner_url"], test_logs)) @@ -909,7 +922,10 @@ class GsePluginDesc(models.Model): scenario_en = models.TextField(_("英文使用场景"), null=True, blank=True) category = models.CharField(_("所属范围"), max_length=32, choices=constants.CATEGORY_CHOICES) launch_node = models.CharField( - _("宿主节点类型要求"), max_length=32, choices=[("agent", "agent"), ("proxy", "proxy"), ("all", "all")], default="all" + _("宿主节点类型要求"), + max_length=32, + choices=[("agent", "agent"), ("proxy", "proxy"), ("all", "all")], + default="all", ) config_file = models.CharField(_("配置文件名称"), max_length=128, null=True, blank=True) @@ -1472,12 +1488,10 @@ class DownloadRecord(models.Model): @property def is_finish(self): - return self.task_status == self.TASK_STATUS_FAILED or self.task_status == self.TASK_STATUS_SUCCESS @property def is_failed(self): - return self.task_status == self.TASK_STATUS_FAILED @property @@ -1973,7 +1987,7 @@ def get_host_id__bk_obj_sub_map( host_id__bk_obj_sub_map[proc_status["bk_host_id"]].append( { "bk_obj_id": proc_status["bk_obj_id"], - "subscription": exist_subscription_id__obj_map.get(int(proc_status["source_id"])) + "subscription": exist_subscription_id__obj_map.get(int(proc_status["source_id"])), # "subscription_id": int(proc_status.source_id), # "name": exist_subscription_id__obj_map.get(int(proc_status.source_id)), } @@ -2229,7 +2243,10 @@ class SubscriptionInstanceRecord(models.Model): is_latest = models.BooleanField(_("是否为实例最新记录"), default=True, db_index=True) status = models.CharField( - _("任务状态"), max_length=45, choices=constants.JobStatusType.get_choices(), default=constants.JobStatusType.PENDING + _("任务状态"), + max_length=45, + choices=constants.JobStatusType.get_choices(), + default=constants.JobStatusType.PENDING, ) @property @@ -2378,7 +2395,10 @@ class SubscriptionInstanceStatusDetail(models.Model): subscription_instance_record_id = models.BigIntegerField(_("订阅实例ID"), db_index=True) node_id = models.CharField(_("Pipeline原子ID"), max_length=50, default="", blank=True, db_index=True) status = models.CharField( - _("任务状态"), max_length=45, choices=constants.JobStatusType.get_choices(), default=constants.JobStatusType.RUNNING + _("任务状态"), + max_length=45, + choices=constants.JobStatusType.get_choices(), + default=constants.JobStatusType.RUNNING, ) log = models.TextField(_("日志内容")) update_time = models.DateTimeField(_("更新时间"), null=True, blank=True, db_index=True) diff --git a/apps/node_man/periodic_tasks/gse_svr_discovery.py b/apps/node_man/periodic_tasks/gse_svr_discovery.py index 5a437ab92..0793674bc 100644 --- a/apps/node_man/periodic_tasks/gse_svr_discovery.py +++ b/apps/node_man/periodic_tasks/gse_svr_discovery.py @@ -9,7 +9,7 @@ specific language governing permissions and limitations under the License. """ from telnetlib import Telnet -from typing import Any, Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple from celery.task import periodic_task from django.conf import settings @@ -21,7 +21,6 @@ def check_ip_ports_reachable(host: str, ports: List[int]) -> bool: - for port in ports: try: with Telnet(host=host, port=port, timeout=2): @@ -33,7 +32,6 @@ def check_ip_ports_reachable(host: str, ports: List[int]) -> bool: class ZkSafeClient: - zk_client: Optional[KazooClient] def __init__(self, hosts: str, auth_data: List[Tuple[str, str]], **kwargs): @@ -110,16 +108,15 @@ def gse_svr_discovery_periodic_task(): continue logger.info(f"zk_node_path -> {zk_node_path}, svr_ips -> {svr_ips}") - inner_ip__outer_ip_map: Dict[str, str] = {} - for svr_info in getattr(ap, ap_field, []): - inner_ip__outer_ip_map[svr_info.get("inner_ip")] = svr_info.get("outer_ip") + outer_ips = getattr(ap, ap_field, {}).get("outer_ip_infos") or {} + inner_ip_infos: List[Dict[str, str]] = [{"ip": inner_ip} for inner_ip in svr_ips] - svr_infos: List[Dict[str, Any]] = [] - for svr_ip in svr_ips: - # svr_ip 通常解析为内网IP,外网IP允许自定义,如果为空再取 svr_ip - outer_ip = inner_ip__outer_ip_map.get(svr_ip) or svr_ip - svr_infos.append({"inner_ip": svr_ip, "outer_ip": outer_ip}) + svr_infos: Dict[str, List[Dict[str, str]]] = { + "inner_ip_infos": inner_ip_infos, + "outer_ip_infos": outer_ips if outer_ips else inner_ip_infos, + } setattr(ap, ap_field, svr_infos) + logger.info(f"update ap -> {ap}, {ap_field} -> {svr_infos}") is_change = True if is_change: ap.save() diff --git a/apps/node_man/serializers/ap.py b/apps/node_man/serializers/ap.py index 9adf3fd8a..cb58b3386 100644 --- a/apps/node_man/serializers/ap.py +++ b/apps/node_man/serializers/ap.py @@ -8,7 +8,7 @@ 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 List +import typing from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -84,28 +84,51 @@ class UpdateOrCreateSerializer(serializers.ModelSerializer): """ class ServersSerializer(serializers.Serializer): - inner_ip = serializers.CharField(label=_("内网IP"), required=False) - inner_ipv6 = serializers.CharField(label=_("内网IPv6"), required=False) - outer_ip = serializers.CharField(label=_("外网IP"), required=False) - outer_ipv6 = serializers.CharField(label=_("外网IPv6"), required=False) - bk_host_id = serializers.IntegerField(label=_("主机ID"), required=False) + class ServerInfoSerializer(serializers.Serializer): + ip = serializers.CharField(label=_("IP地址"), required=False) + bk_host_id = serializers.IntegerField(label=_("主机ID"), required=False) - def validate(self, attrs): - basic.ipv6_formatter(data=attrs, ipv6_field_names=["inner_ipv6", "outer_ipv6"]) + inner_ip_infos = serializers.ListField(label=_("内网IP信息"), required=False, child=ServerInfoSerializer()) + outer_ip_infos = serializers.ListField(label=_("外网IP信息"), required=False, child=ServerInfoSerializer()) - if not (attrs.get("inner_ip") or attrs.get("inner_ipv6")): - raise ValidationError(_("请求参数 inner_ip 和 inner_ipv6 不能同时为空")) - if not (attrs.get("outer_ip") or attrs.get("outer_ipv6")): - raise ValidationError(_("请求参数 outer_ip 和 outer_ipv6 不能同时为空")) + def validate(self, attrs): + if not attrs.get("inner_ip_infos") and not attrs.get("outer_ip_infos"): + raise ValidationError(_("请求参数 inner_ip_info, outer_ip_infos 不可同时为空")) + + for attr in attrs.keys(): + v4_ips, abnormal_ips, format_ipv6_infos = [], [], [] + for ip_info in attrs.get(attr, []): + ip = ip_info.get("ip", None) + if not ip: + continue + if basic.is_v4(ip): + v4_ips.append(ip_info) + elif basic.is_v6(ip): + ip_info.update({"ip": basic.exploded_ip(ip)}) + format_ipv6_infos.append(ip_info) + else: + # 不是有效的 IP 地址 + abnormal_ips.append(ip_info) + + if v4_ips and format_ipv6_infos: + raise ValidationError(_(f"{attr} 中不能同时包括 ipv4 和 ipv6")) + if abnormal_ips: + raise ValidationError(_(f"{attr} 中存在非法 IP 地址: {abnormal_ips}")) + + if format_ipv6_infos: + attrs[attr] = format_ipv6_infos + + # 去重复 + attrs[attr] = list({frozenset(d.items()): d for d in attrs[attr] if d is not None}.values()) return attrs class ZKSerializer(serializers.Serializer): zk_ip = serializers.CharField(label=_("ZK IP地址")) zk_port = serializers.CharField(label=_("ZK 端口")) - btfileserver = serializers.ListField(child=ServersSerializer()) - dataserver = serializers.ListField(child=ServersSerializer()) - taskserver = serializers.ListField(child=ServersSerializer()) + btfileserver = ServersSerializer() + dataserver = ServersSerializer() + taskserver = ServersSerializer() zk_hosts = serializers.ListField(child=ZKSerializer()) zk_account = serializers.CharField(label=_("ZK账号"), required=False, allow_blank=True) zk_password = serializers.CharField(label=_("ZK密码"), required=False, allow_blank=True) @@ -119,7 +142,7 @@ class ZKSerializer(serializers.Serializer): callback_url = serializers.CharField(label=_("节点管理内网回调地址"), required=False, allow_blank=True) def validate(self, data): - gse_version_list: List[str] = list(set(AccessPoint.objects.values_list("gse_version", flat=True))) + gse_version_list: typing.List[str] = list(set(AccessPoint.objects.values_list("gse_version", flat=True))) # 存量接入点版本全部为V2新建/更新版本也为V2版本 if GseVersion.V1.value not in gse_version_list: data["gse_version"] = GseVersion.V2.value diff --git a/apps/node_man/tests/test_pericdic_tasks/mock_data.py b/apps/node_man/tests/test_pericdic_tasks/mock_data.py index 0ec2a3c30..f188e5018 100644 --- a/apps/node_man/tests/test_pericdic_tasks/mock_data.py +++ b/apps/node_man/tests/test_pericdic_tasks/mock_data.py @@ -128,8 +128,15 @@ "/gse/config/server/task/all": ["127.0.0.1", "127.0.0.2", "127.0.0.3"], "/gse/config/server/btfiles/all": ["127.0.0.1", "127.0.0.2", "127.0.0.3"], } -MOCK_AP_FIELD_MAP = [ - {"inner_ip": "127.0.0.1", "outer_ip": "127.0.0.1"}, - {"inner_ip": "127.0.0.2", "outer_ip": "127.0.0.2"}, - {"inner_ip": "127.0.0.3", "outer_ip": "127.0.0.3"}, -] +MOCK_AP_FIELD_MAP = { + "inner_ip_infos": [ + {"ip": "127.0.0.1"}, + {"ip": "127.0.0.2"}, + {"ip": "127.0.0.3"}, + ], + "outer_ip_infos": [ + {"ip": "127.0.0.1"}, + {"ip": "127.0.0.2"}, + {"ip": "127.0.0.3"}, + ], +} diff --git a/apps/node_man/tests/test_pericdic_tasks/test_gse_svr_discovery.py b/apps/node_man/tests/test_pericdic_tasks/test_gse_svr_discovery.py index fbefa1de6..8baea6702 100644 --- a/apps/node_man/tests/test_pericdic_tasks/test_gse_svr_discovery.py +++ b/apps/node_man/tests/test_pericdic_tasks/test_gse_svr_discovery.py @@ -38,7 +38,7 @@ def test_gse_svr_discovery(self): # 检查ap_field是否已经更新。注: 如果gse_svr_discovery的ap_field更改了,单测这里也需要同步更改 ap_field_list = ["dataserver", "dataserver", "btfileserver"] for ap_field in ap_field_list: - self.assertEqual(getattr(ap, ap_field, []), MOCK_AP_FIELD_MAP) + self.assertEqual(getattr(ap, ap_field, {}), MOCK_AP_FIELD_MAP) class TestGseSvrDiscoveryEmptyRegionCity(TestGseSvrDiscovery): @@ -47,3 +47,27 @@ def setUpTestData(cls): ap = AccessPoint.objects.all().first() ap.city_id = ap.region_id = None ap.save() + + +class TestExistOuterIpInfoDiscovery(CustomBaseTestCase): + @classmethod + def setUpTestData(cls): + ap = AccessPoint.objects.all().first() + ap.city_id = ap.region_id = "test" + ap.btfileserver = {"inner_ip_infos": [{"ip": "127.0.0.1"}], "outer_ip_infos": [{"ip": "121.0.0.1"}]} + ap.save() + + @patch("apps.node_man.periodic_tasks.gse_svr_discovery.settings.GSE_ENABLE_SVR_DISCOVERY", True) + @patch("apps.node_man.periodic_tasks.gse_svr_discovery.KazooClient", MockKazooClient) + @patch("apps.node_man.periodic_tasks.gse_svr_discovery.check_ip_ports_reachable", check_ip_ports_reachable) + def test_gse_bt_svr_discovery(self): + gse_svr_discovery_periodic_task() + ap = AccessPoint.objects.all().first() + + self.assertEqual( + ap.btfileserver, + { + "inner_ip_infos": [{"ip": "127.0.0.1"}, {"ip": "127.0.0.2"}, {"ip": "127.0.0.3"}], + "outer_ip_infos": [{"ip": "121.0.0.1"}], + }, + ) diff --git a/apps/node_man/tests/test_tools/test_ap_transform.py b/apps/node_man/tests/test_tools/test_ap_transform.py new file mode 100644 index 000000000..e6d5a7450 --- /dev/null +++ b/apps/node_man/tests/test_tools/test_ap_transform.py @@ -0,0 +1,639 @@ +# -*- coding: utf-8 -*- +""" +TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-节点管理(BlueKing-BK-NODEMAN) available. +Copyright (C) 2017-2022 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 copy import deepcopy + +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase + +from apps.mock_data.common_unit.host import ( + AP_MODEL_DATA, + DEFAULT_HOST_ID, + DEFAULT_IP, + DEFAULT_IPV6, +) +from apps.node_man import models +from apps.node_man.utils.endpoint import Endpoint, EndPointTransform +from apps.utils.basic import exploded_ip +from apps.utils.unittest.testcase import CustomAPITestCase +from env.constants import GseVersion + + +class TestApTransform(TestCase): + def mock_outer_ipv6(self, ipv6: str): + return DEFAULT_IPV6.replace("6", "A") + + def setUp(self): + test_data_list = [ + { + "name": "公有云接入点", + "ap_type": "system", + "region_id": "2", + "city_id": "30", + "gse_version": "V2", + "btfileserver": [ + {"inner_ip": "127.0.0.120", "outer_ip": "127.0.0.20", "inner_ipv6": None, "outer_ipv6": None}, + { + "inner_ip": "127.0.0.27", + "outer_ip": "127.0.0.27", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "dataserver": [ + {"inner_ip": "127.0.0.120", "outer_ip": "127.0.0.120", "inner_ipv6": None, "outer_ipv6": None}, + { + "inner_ip": "127.0.0.27", + "outer_ip": "127.0.0.27", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "taskserver": [ + {"inner_ip": "127.0.0.120", "outer_ip": "127.0.0.120", "inner_ipv6": None, "outer_ipv6": None}, + { + "inner_ip": "127.0.0.27", + "outer_ip": "127.0.0.27", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "zk_hosts": [{"zk_ip": "127.0.0.190", "zk_port": "2182"}], + "zk_account": "", + "zk_password": "", + "package_inner_url": "http://nodeman.test.com/download/prod", + "package_outer_url": "http://127.0.0.161/download/", + "nginx_path": "/data/bkee/public/bknodeman/download", + "agent_config": { + "linux": { + "dataipc": "/usr/local/gse/agent/data/ipc.state.report", + "log_path": "/var/log/gse", + "run_path": "/var/run/gse", + "data_path": "/var/lib/gse", + "temp_path": "/tmp", + "setup_path": "/usr/local/gse", + "hostid_path": "/var/lib/gse/host/hostid", + }, + "windows": { + "dataipc": 27002, + "log_path": "C:\\gse\\logs", + "run_path": "C:\\gse\\data", + "data_path": "C:\\gse\\data", + "temp_path": "C:\\Temp", + "setup_path": "C:\\gse", + "hostid_path": "C:\\gse\\data\\host\\hostid", + }, + }, + "status": None, + "description": "GSE2_上海外网", + "is_enabled": True, + "is_default": True, + "creator": ["admin"], + "port_config": { + "bt_port": 20020, + "io_port": 28668, + "data_port": 28625, + "proc_port": 50000, + "trunk_port": 48329, + "bt_port_end": 60030, + "tracker_port": 20030, + "bt_port_start": 60020, + "db_proxy_port": 58859, + "file_svr_port": 28925, + "api_server_port": 50002, + "file_svr_port_v1": 58926, + "agent_thrift_port": 48669, + "btsvr_thrift_port": 58931, + "data_prometheus_port": 29402, + "file_metric_bind_port": 29404, + "file_topology_bind_port": 28930, + }, + "proxy_package": ["gse_client-windows-x86_64.tgz", "gse_client-linux-x86_64.tgz"], + "outer_callback_url": "", + "callback_url": "", + }, + { + "name": "公有云接入点_v1", + "ap_type": "system", + "region_id": "test", + "city_id": "test", + "gse_version": "V1", + "btfileserver": [ + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.69", + "inner_ipv6": None, + "outer_ipv6": None, + }, + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.76", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "dataserver": [ + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.69", + "inner_ipv6": None, + "outer_ipv6": None, + }, + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.76", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "taskserver": [ + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.69", + "inner_ipv6": None, + "outer_ipv6": None, + }, + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.76", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "zk_hosts": [{"zk_ip": DEFAULT_IP, "zk_port": "2182"}], + "zk_account": "noneed", + "zk_password": "noneed", + "package_inner_url": "http://nodeman.test.com/download/prod-oa", + "package_outer_url": "http://127.0.0.1/download/prod-oa", + "nginx_path": "/data/bkee/public/bknodeman/download/prod-oa", + "agent_config": { + "linux": { + "dataipc": "/usr/local/gse/agent/data/ipc.state.report", + "log_path": "/var/log/gse", + "run_path": "/var/run/gse", + "data_path": "/var/lib/gse", + "temp_path": "/tmp", + "setup_path": "/usr/local/gse", + "hostid_path": "/var/lib/gse/host/hostid", + }, + "windows": { + "dataipc": "47000", + "log_path": "C:\\gse\\logs", + "run_path": "C:\\gse\\data", + "data_path": "C:\\gse\\data", + "temp_path": "C:\\Temp", + "setup_path": "C:\\gse", + "hostid_path": "C:\\gse\\data\\host\\hostid", + }, + }, + "status": None, + "description": "专用Proxy请勿选择", + "is_enabled": True, + "is_default": False, + "creator": ["admin"], + "port_config": { + "bt_port": 10020, + "io_port": 48668, + "data_port": 58625, + "proc_port": 50000, + "trunk_port": 48329, + "bt_port_end": 60030, + "tracker_port": 10030, + "bt_port_start": 60020, + "db_proxy_port": 58859, + "file_svr_port": 58925, + "api_server_port": 50002, + "agent_thrift_port": 48669, + "btsvr_thrift_port": 58930, + "data_prometheus_port": 59402, + }, + "proxy_package": [ + "gse_client-windows-x86_64.tgz", + "gse_client-linux-x86_64.tgz", + "gse_client-linux-aarch64.tgz", + ], + "outer_callback_url": "", + "callback_url": "", + }, + { + "name": "GSE2_IPV6", + "ap_type": "system", + "region_id": "2", + "city_id": "30", + "gse_version": "V2", + "btfileserver": [ + { + "inner_ip": "", + "outer_ip": "", + "inner_ipv6": DEFAULT_IPV6, + "outer_ipv6": self.mock_outer_ipv6(DEFAULT_IPV6), + }, + { + "inner_ip": "", + "outer_ip": "", + "inner_ipv6": DEFAULT_IPV6, + "outer_ipv6": self.mock_outer_ipv6(DEFAULT_IPV6), + }, + ], + "dataserver": [ + { + "inner_ip": "", + "outer_ip": "", + "inner_ipv6": DEFAULT_IPV6, + "outer_ipv6": self.mock_outer_ipv6(DEFAULT_IPV6), + }, + { + "inner_ip": "", + "outer_ip": "", + "inner_ipv6": DEFAULT_IPV6, + "outer_ipv6": self.mock_outer_ipv6(DEFAULT_IPV6), + }, + ], + "taskserver": [ + { + "inner_ip": "", + "outer_ip": "", + "inner_ipv6": DEFAULT_IPV6, + "outer_ipv6": self.mock_outer_ipv6(DEFAULT_IPV6), + }, + { + "inner_ip": "", + "outer_ip": "", + "inner_ipv6": DEFAULT_IPV6, + "outer_ipv6": self.mock_outer_ipv6(DEFAULT_IPV6), + }, + ], + "zk_hosts": [{"zk_ip": DEFAULT_IP, "zk_port": "2182"}], + "zk_account": "noneed", + "zk_password": "noneed", + "package_inner_url": "http://nodeman.test.com/download/prod-oa", + "package_outer_url": "http://127.0.0.1/download/", + "nginx_path": "/data/bkee/public/bknodeman/download/", + "agent_config": { + "linux": { + "dataipc": "/usr/local/gse/agent/data/ipc.state.report", + "log_path": "/var/log/gse", + "run_path": "/var/run/gse", + "data_path": "/var/lib/gse", + "temp_path": "/tmp", + "setup_path": "/usr/local/gse", + "hostid_path": "/var/lib/gse/host/hostid", + }, + "windows": { + "dataipc": 27002, + "log_path": "C:\\gse\\logs", + "run_path": "C:\\gse\\data", + "data_path": "C:\\gse\\data", + "temp_path": "C:\\Temp", + "setup_path": "C:\\gse", + "hostid_path": "C:\\gse\\data\\host\\hostid", + }, + }, + "status": None, + "description": "专用Proxy请勿选择", + "is_enabled": True, + "is_default": False, + "creator": ["admin"], + "port_config": { + "bt_port": 20020, + "io_port": 28668, + "data_port": 28625, + "proc_port": 50000, + "trunk_port": 48329, + "bt_port_end": 60030, + "tracker_port": 20030, + "bt_port_start": 60020, + "db_proxy_port": 58859, + "file_svr_port": 28925, + "api_server_port": 50002, + "file_svr_port_v1": 58926, + "agent_thrift_port": 48669, + "btsvr_thrift_port": 58931, + "file_metric_bind_port": 29404, + "file_topology_bind_port": 28930, + }, + "proxy_package": ["gse_client-windows-x86_64.tgz", "gse_client-linux-x86_64.tgz"], + "outer_callback_url": "", + "callback_url": "", + }, + { + "name": "内外网相同接入点", + "ap_type": "system", + "region_id": "2", + "city_id": "30", + "gse_version": "V2", + "btfileserver": [ + {"inner_ip": "127.0.0.120", "outer_ip": "127.0.0.120", "inner_ipv6": None, "outer_ipv6": None}, + ], + "dataserver": [ + {"inner_ip": "127.0.0.120", "outer_ip": "127.0.0.120", "inner_ipv6": None, "outer_ipv6": None}, + { + "inner_ip": "127.0.0.27", + "outer_ip": "127.0.0.27", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "taskserver": [ + {"inner_ip": "127.0.0.120", "outer_ip": "127.0.0.120", "inner_ipv6": None, "outer_ipv6": None}, + { + "inner_ip": "127.0.0.27", + "outer_ip": "127.0.0.27", + "inner_ipv6": None, + "outer_ipv6": None, + }, + ], + "zk_hosts": [{"zk_ip": "127.0.0.190", "zk_port": "2182"}], + "zk_account": "", + "zk_password": "", + "package_inner_url": "http://nodeman.test.com/download/prod", + "package_outer_url": "http://127.0.0.161/download/", + "nginx_path": "/data/bkee/public/bknodeman/download", + "agent_config": { + "linux": { + "dataipc": "/usr/local/gse/agent/data/ipc.state.report", + "log_path": "/var/log/gse", + "run_path": "/var/run/gse", + "data_path": "/var/lib/gse", + "temp_path": "/tmp", + "setup_path": "/usr/local/gse", + "hostid_path": "/var/lib/gse/host/hostid", + }, + "windows": { + "dataipc": 27002, + "log_path": "C:\\gse\\logs", + "run_path": "C:\\gse\\data", + "data_path": "C:\\gse\\data", + "temp_path": "C:\\Temp", + "setup_path": "C:\\gse", + "hostid_path": "C:\\gse\\data\\host\\hostid", + }, + }, + }, + ] + + for ap_data in test_data_list: + models.AccessPoint.objects.update_or_create(**ap_data) + + def test_ap_transform(self): + gse_v1_ap = models.AccessPoint.objects.get(gse_version=GseVersion.V1.value, name="公有云接入点_v1") + self.assertEqual( + EndPointTransform().transform(gse_v1_ap.taskserver), + { + "inner_ip_infos": [{"ip": "127.0.0.198"}], + "outer_ip_infos": [{"ip": "127.0.0.69"}, {"ip": "127.0.0.76"}], + }, + ) + + gse_v2_ap = models.AccessPoint.objects.get(gse_version=GseVersion.V2.value, name="公有云接入点") + self.assertEqual( + EndPointTransform().transform(gse_v2_ap.btfileserver), + { + "inner_ip_infos": [{"ip": "127.0.0.120"}, {"ip": "127.0.0.27"}], + "outer_ip_infos": [{"ip": "127.0.0.20"}, {"ip": "127.0.0.27"}], + }, + ) + outer_and_inner_same_ip_ap = models.AccessPoint.objects.get(name="内外网相同接入点") + self.assertEqual( + EndPointTransform().transform(outer_and_inner_same_ip_ap.btfileserver), + { + "inner_ip_infos": [{"ip": "127.0.0.120"}], + "outer_ip_infos": [{"ip": "127.0.0.120"}], + }, + ) + + def test_transfrom_command(self): + # 调用 django command transform_ap_data 进行数据转换 + default_ap_id = models.AccessPoint.objects.get(name="默认接入点").id + # 因为默认的接入点已经经过转换,所以这里需要先把默认接入点的数据转换为旧的格式 + call_command("transform_ap_data", transform_endpoint_to_legacy=True, transform_ap_id=default_ap_id) + default_ap_obj = models.AccessPoint.objects.get(name="默认接入点") + self.assertEqual( + default_ap_obj.taskserver, + [ + { + "inner_ip": "", + "outer_ip": "", + } + ], + ) + # 把所有的接入点都转换一遍, 转换为新的格式 + call_command("transform_ap_data", transform=True, all_ap=True) + for ap in models.AccessPoint.objects.all(): + self.assertTrue(isinstance(ap.taskserver, dict)) + + gse_v1_ap = models.AccessPoint.objects.get(gse_version="V1", name="公有云接入点_v1") + self.assertEqual( + gse_v1_ap.btfileserver, + { + "inner_ip_infos": [{"ip": "127.0.0.198"}], + "outer_ip_infos": [{"ip": "127.0.0.69"}, {"ip": "127.0.0.76"}], + }, + ) + v6_ap_obj = models.AccessPoint.objects.get(name="GSE2_IPV6") + self.assertEqual( + v6_ap_obj.btfileserver, + { + "inner_ip_infos": [{"ip": DEFAULT_IPV6}], + "outer_ip_infos": [{"ip": self.mock_outer_ipv6(DEFAULT_IPV6)}], + }, + ) + + # 转换回旧的数据 + call_command("transform_ap_data", transform_endpoint_to_legacy=True, transform_ap_id=gse_v1_ap.id) + self.assertEqual( + # 转换回旧的数据,并且过滤为 None 的字段 + models.AccessPoint.objects.get(id=gse_v1_ap.id).btfileserver, + [ + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.69", + }, + { + "inner_ip": "127.0.0.198", + "outer_ip": "127.0.0.76", + }, + ], + ) + call_command("transform_ap_data", transform=True, transform_ap_id=gse_v1_ap.id) + self.assertEqual( + models.AccessPoint.objects.get(id=gse_v1_ap.id).btfileserver, + { + "inner_ip_infos": [{"ip": "127.0.0.198"}], + "outer_ip_infos": [{"ip": "127.0.0.69"}, {"ip": "127.0.0.76"}], + }, + ) + + # 校验参数 + self.assertRaises( + CommandError, call_command, "transform_ap_data", transform_endpoint_to_legacy=True, transform=True + ) + self.assertRaises( + CommandError, call_command, "transform_ap_data", transform_endpoint_to_legacy=True, transform=False + ) + self.assertRaises( + CommandError, + call_command, + "transform_ap_data", + transform_endpoint_to_legacy=True, + transform=False, + all_ap=True, + transform_ap_id=gse_v1_ap.id, + ) + + def test_transform_with_host_id(self): + ap = models.AccessPoint.objects.get(name="公有云接入点_v1") + ap_btfileserver = ap.btfileserver + for server in ap_btfileserver: + server.update(bk_host_id=DEFAULT_HOST_ID) + models.AccessPoint.objects.filter(name="公有云接入点_v1").update(btfileserver=ap_btfileserver) + call_command("transform_ap_data", transform=True, transform_ap_id=ap.id) + self.assertEqual( + models.AccessPoint.objects.get(id=ap.id).btfileserver, + { + "inner_ip_infos": [{"ip": "127.0.0.198", "bk_host_id": DEFAULT_HOST_ID}], + "outer_ip_infos": [ + {"ip": "127.0.0.69", "bk_host_id": DEFAULT_HOST_ID}, + {"ip": "127.0.0.76", "bk_host_id": DEFAULT_HOST_ID}, + ], + }, + ) + + # test ap endpoint + self.assertEqual( + models.AccessPoint.objects.get(id=ap.id).file_endpoint_info.outer_hosts, ["127.0.0.69", "127.0.0.76"] + ) + self.assertEqual(models.AccessPoint.objects.get(id=ap.id).file_endpoint_info.inner_hosts, ["127.0.0.198"]) + self.assertEqual( + models.AccessPoint.objects.get(id=ap.id).file_endpoint_info.inner_endpoints, + [Endpoint(v4="127.0.0.198", v6=None, host_id=DEFAULT_HOST_ID)], + ) + + +class ApViewTransformTestCase(CustomAPITestCase): + TEST_AP_NAME = "CUSTOM_AP" + CREATE_URL = "/api/ap/" + + def setUp(self): + ap_data = deepcopy(AP_MODEL_DATA) + ap_data["name"] = self.TEST_AP_NAME + self.ap_data = ap_data + + def test_ap_create(self): + self.client.post(path=self.CREATE_URL, data=self.ap_data) + ap = models.AccessPoint.objects.get(name=self.TEST_AP_NAME) + self.assertEqual(ap.name, self.TEST_AP_NAME) + self.assertEqual(ap.taskserver, AP_MODEL_DATA["taskserver"]) + + def test_mix_ip_ap_create(self): + self.ap_data["taskserver"] = {"inner_ip_infos": [{"ip": DEFAULT_IP}, {"ip": DEFAULT_IPV6}]} + result = self.client.post(self.CREATE_URL, self.ap_data) + self.assertFalse(result["result"]) + self.assertEqual(result["message"], "inner_ip_infos 中不能同时包括 ipv4 和 ipv6(3800001)") + + # 支持 taskserver v6 & fileserver v4 混合 + mix_server_ap_data = deepcopy(self.ap_data) + mix_server_ap_data["taskserver"] = {"inner_ip_infos": [{"ip": DEFAULT_IPV6}]} + result = self.client.post(self.CREATE_URL, mix_server_ap_data) + self.assertTrue(result["result"]) + + def test_multi_v4_ap_create(self): + # 支持多个 v4 地址 + self.ap_data["taskserver"] = { + "inner_ip_infos": [{"ip": DEFAULT_IP}, {"ip": DEFAULT_IP.replace("1", "2")}], + "outer_ip_infos": [{"ip": DEFAULT_IP}, {"ip": DEFAULT_IP.replace("1", "2")}], + } + result = self.client.post(self.CREATE_URL, self.ap_data) + self.assertTrue(result["result"]) + ap = models.AccessPoint.objects.get(name=self.TEST_AP_NAME) + self.assertEqual( + ap.taskserver, + { + "inner_ip_infos": [{"ip": DEFAULT_IP}, {"ip": DEFAULT_IP.replace("1", "2")}], + "outer_ip_infos": [{"ip": DEFAULT_IP}, {"ip": DEFAULT_IP.replace("1", "2")}], + }, + ) + + def test_multi_v6_ap_create(self): + # 支持多个 v6 地址 + self.ap_data["taskserver"] = {"inner_ip_infos": [{"ip": DEFAULT_IPV6}, {"ip": DEFAULT_IPV6.replace("1", "2")}]} + result = self.client.post(self.CREATE_URL, self.ap_data) + self.assertTrue(result["result"]) + + def test_v4_v6_mix_ap_create(self): + # 支持同一个 server inner & outer v4 v6 混合 + self.ap_data["taskserver"] = { + "inner_ip_infos": [{"ip": DEFAULT_IP}], + "outer_ip_infos": [{"ip": DEFAULT_IPV6}], + } + result = self.client.post(self.CREATE_URL, self.ap_data) + self.assertTrue(result["result"]) + ap = models.AccessPoint.objects.get(name=self.TEST_AP_NAME) + self.assertEqual( + ap.taskserver, + { + "inner_ip_infos": [{"ip": DEFAULT_IP}], + "outer_ip_infos": [{"ip": exploded_ip(DEFAULT_IPV6)}], + }, + ) + + def test_filter_ip_ap_crete(self): + # 支持过滤掉重复 ip + self.ap_data["taskserver"] = {"inner_ip_infos": [{"ip": DEFAULT_IP}, {"ip": DEFAULT_IP}]} + result = self.client.post(self.CREATE_URL, self.ap_data) + self.assertTrue(result["result"]) + self.assertEqual( + result["data"]["taskserver"]["inner_ip_infos"], + [{"ip": DEFAULT_IP}], + ) + + def test_illegal_ip_ap_create(self): + # 支持 v4 / v6 IP格式检测 + self.ap_data["taskserver"] = {"inner_ips": [{"inner_ip": "11"}]} + self.assertFalse(self.client.post(self.CREATE_URL, self.ap_data)["result"]) + self.ap_data["taskserver"] = {"inner_ips": [{"inner_ip": DEFAULT_IP}, {"inner_ip": "11"}]} + self.assertFalse(self.client.post(self.CREATE_URL, self.ap_data)["result"]) + self.ap_data["taskserver"] = {"inner_ips": [{"inner_ipv6": DEFAULT_IP}]} + self.assertFalse(self.client.post(self.CREATE_URL, self.ap_data)["result"]) + + def test_update_ap(self): + update_ap_name = "默认update ap" + self.ap_data["taskserver"] = {"inner_ip_infos": [{"ip": "111.1.1.1"}]} + self.ap_data["name"] = "默认update ap" + ap_id = models.AccessPoint.objects.get(name="默认接入点").id + update_url = f"/api/ap/{ap_id}/" + result = self.client.put(update_url, self.ap_data) + self.assertTrue(result["result"]) + ap_obj = models.AccessPoint.objects.get(name=update_ap_name) + self.assertEqual(ap_obj.taskserver, {"inner_ip_infos": [{"ip": "111.1.1.1"}]}) + + def test_ap_retrieve(self): + self.test_ap_create() + ap_id = models.AccessPoint.objects.get(name=self.TEST_AP_NAME).id + retrieve_url = f"/api/ap/{ap_id}/" + result = self.client.get(retrieve_url) + self.assertTrue(result["result"]) + self.assertEqual(result["data"]["name"], self.TEST_AP_NAME) + self.assertEqual(result["data"]["taskserver"], AP_MODEL_DATA["taskserver"]) + + def test_ap_test(self): + ap_tes_url: str = "/api/ap/test/" + test_ap_data: dict = { + "btfileserver": {"inner_ip_infos": [{"ip": DEFAULT_IP}], "outer_ip_infos": [{"ip": "127.0.0.2"}]}, + "taskserver": {"inner_ip_infos": [{"ip": DEFAULT_IP}], "outer_ip_infos": [{"ip": "127.0.0.2"}]}, + "dataserver": {"inner_ip_infos": [{"ip": DEFAULT_IP}], "outer_ip_infos": [{"ip": "127.0.0.2"}]}, + "package_inner_url": "http://127.0.0.1/download/", + "package_outer_url": "http://127.0.0.2/download/", + } + result = self.client.post(ap_tes_url, test_ap_data) + self.assertFalse(result["data"]["test_result"]) + self.assertEqual(result["data"]["test_logs"][1]["log"], f"Ping {DEFAULT_IP} 正常") diff --git a/apps/node_man/tests/utils.py b/apps/node_man/tests/utils.py index 576f7883d..6583c38f3 100644 --- a/apps/node_man/tests/utils.py +++ b/apps/node_man/tests/utils.py @@ -192,7 +192,6 @@ def create_host( class Subscription: def create_subscription(self, job_type, nodes, *args, **kwargs): - cipher = tools.HostTools.get_asymmetric_cipher() subscription_id = random.randint(100, 1000) task_id = random.randint(10, 1000) @@ -659,9 +658,12 @@ def create_ap(number): "is_enabled": 1, "zk_account": "bkzk", "zk_password": "bkzk", - "btfileserver": [{"inner_ip": random_ip(), "outer_ip": random_ip()}], - "dataserver": [{"inner_ip": random_ip(), "outer_ip": random_ip()}], - "taskserver": [{"inner_ip": random_ip(), "outer_ip": random_ip()}], + "btfileserver": { + "inner_ips": [{"inner_ip": random_ip()}], + "outer_ips": [{"outer_ip": random_ip()}], + }, + "dataserver": {"inner_ips": [{"inner_ip": random_ip()}], "outer_ips": [{"outer_ip": random_ip()}]}, + "taskserver": {"inner_ips": [{"inner_ip": random_ip()}], "outer_ips": [{"outer_ip": random_ip()}]}, "package_inner_url": "http://127.0.0.1:80/download", "package_outer_url": "http://127.0.0.1:80/download", "nginx_path": "/data/bkee/public/bknodeman/download", diff --git a/apps/node_man/utils/endpoint.py b/apps/node_man/utils/endpoint.py index 62e8757f4..b2f8d182f 100644 --- a/apps/node_man/utils/endpoint.py +++ b/apps/node_man/utils/endpoint.py @@ -9,9 +9,15 @@ specific language governing permissions and limitations under the License. """ +import json import typing +from collections import defaultdict from dataclasses import dataclass, field +from apps.utils.basic import filter_values, is_v4, is_v6 +from apps.utils.md5 import _count_md5 +from common.log import logger + @dataclass class Endpoint: @@ -27,8 +33,8 @@ def __post_init__(self): class EndpointInfo: def __init__( self, - inner_server_infos: typing.List[typing.Dict[str, typing.Any]], - outer_server_infos: typing.List[typing.Dict[str, typing.Any]], + inner_ip_infos: typing.List[typing.Dict[str, typing.Union[str, int]]], + outer_ip_infos: typing.List[typing.Dict[str, typing.Union[str, int]]], ): self.inner_endpoints: typing.List[Endpoint] = [] self.outer_endpoints: typing.List[Endpoint] = [] @@ -36,22 +42,101 @@ def __init__( self.outer_hosts: typing.List[str] = [] self.inner_hosts: typing.List[str] = [] - for inner_server_info in inner_server_infos: - endpoint: Endpoint = Endpoint( - v4=inner_server_info.get("inner_ip"), - v6=inner_server_info.get("inner_ipv6"), - host_id=inner_server_info.get("host_id"), - ) - self.inner_endpoints.append(endpoint) - if endpoint.host_str: - self.inner_hosts.append(endpoint.host_str) - - for outer_server_info in outer_server_infos: - endpoint: Endpoint = Endpoint( - v4=outer_server_info.get("outer_ip"), - v6=outer_server_info.get("outer_ipv6"), - host_id=outer_server_info.get("host_id"), - ) - self.outer_endpoints.append(endpoint) - if endpoint.host_str: - self.outer_hosts.append(endpoint.host_str) + def create_endpoint(ip_info: typing.Dict[str, typing.Union[str, int]]) -> Endpoint: + ip = ip_info.get("ip") + host_id = ip_info.get("bk_host_id") + if is_v4(ip): + return Endpoint(v4=ip, v6=None, host_id=host_id) + else: + return Endpoint(v6=ip, v4=None, host_id=host_id) + + self.inner_endpoints = [create_endpoint(ip_info) for ip_info in inner_ip_infos] + self.outer_endpoints = [create_endpoint(ip_info) for ip_info in outer_ip_infos] + + self.inner_hosts = [endpoint.host_str for endpoint in self.inner_endpoints if endpoint.host_str] + self.outer_hosts = [endpoint.host_str for endpoint in self.outer_endpoints if endpoint.host_str] + + +class EndPointTransform(object): + # legacy_endpoint: [{ "inner_ip": "10.0.6.44", "outer_ip": "10.0.6.44"}, {"inner_ip": "xxx", "outer_ip": "xxx"}] + # endpoint: { + # "inner_ip_infos": [{"ip": "", "bk_host_id": x], + # "outer_ip_infos": [{"ip": "", "bk_host_id": x] + # } + + def transform( + self, legacy_endpoints: typing.List[typing.Dict[str, typing.Any]] + ) -> typing.Dict[str, typing.List[typing.Dict[str, typing.Union[str, int]]]]: + endpoints = defaultdict(list) + if not isinstance(legacy_endpoints, list): + raise ValueError("legacy_endpoints must be list") + for legacy_endpoint in legacy_endpoints: + inner_ip = legacy_endpoint.get("inner_ip") or legacy_endpoint.get("inner_ipv6") + outer_ip = legacy_endpoint.get("outer_ip") or legacy_endpoint.get("outer_ipv6") + bk_host_id = legacy_endpoint.get("bk_host_id") + if inner_ip: + endpoints["inner_ip_infos"].append( + { + "bk_host_id": bk_host_id, + "ip": legacy_endpoint.get("inner_ip") or legacy_endpoint.get("inner_ipv6"), + } + ) + if outer_ip: + endpoints["outer_ip_infos"].append( + { + "ip": outer_ip, + "bk_host_id": bk_host_id, + } + ) + # 把 endpoints 的 values 包括的字典中的空字段去掉 并且去重 + unique_endpoints = defaultdict(list) + seen = set() + for endpoint_type in endpoints: + for endpoint in endpoints[endpoint_type]: + hash_value = _count_md5(json.dumps(endpoint)) + if hash_value not in seen: + unique_endpoints[endpoint_type].append(filter_values(endpoint)) + seen.add(hash_value) + logger.info(f"unique_endpoints: {unique_endpoints}, endpoints: {endpoints}") + return dict(unique_endpoints) + + def transform_endpoint_to_legacy( + self, + endpoints: typing.Dict[str, typing.List[typing.Dict[str, typing.Union[str, int]]]], + ) -> typing.List[typing.Dict[str, typing.Any]]: + legacy_endpoints = [] + if not isinstance(endpoints, dict): + raise ValueError("endpoints must be dict") + + # endpoints 的 key 必须为这两个值,无序的 ["inner_ip_infos", "outer_ip_infos"]: + legacy_endpoints = [] + for endpoint_type in endpoints: + if endpoint_type not in ["inner_ip_infos", "outer_ip_infos"]: + raise ValueError("endpoints key must be in inner_ip_infos or outer_ip_infos") + if not isinstance(endpoints[endpoint_type], list): + raise ValueError("endpoints value must be list") + + # 这里保留之前的为空的数据默认字段 inner_ip & outer_ip 的值为空字符串, 其他字段过滤掉 + legacy_endpoints = [ + { + "inner_ip": inner_info.get("ip") if is_v4(inner_info.get("ip")) else "", + "outer_ip": outer_info.get("ip") if is_v4(outer_info.get("ip")) else "", + "inner_ipv6": inner_info.get("ip") if is_v6(inner_info.get("ip")) else None, + "outer_ipv6": outer_info.get("ip") if is_v6(outer_info.get("ip")) else None, + "bk_host_id": inner_info.get("bk_host_id") or outer_info.get("bk_host_id"), + } + for inner_info in endpoints.get("inner_ip_infos", []) + for outer_info in endpoints.get("outer_ip_infos", []) + ] + + # 把 legacy_endpoints 通过字典的 md5 去重 + seen = set() + unique_endpoints = [] + for endpoint in legacy_endpoints: + hash_value = _count_md5(json.dumps(endpoint)) + if hash_value not in seen: + unique_endpoints.append(filter_values(endpoint)) + seen.add(hash_value) + logger.info(f"unique_endpoints: {unique_endpoints}, legacy_endpoints: {legacy_endpoints}") + + return unique_endpoints diff --git a/apps/node_man/views/ap.py b/apps/node_man/views/ap.py index dca9ca9f4..71057c4d4 100644 --- a/apps/node_man/views/ap.py +++ b/apps/node_man/views/ap.py @@ -57,12 +57,18 @@ def list(self, request, *args, **kwargs): ] "zk_user": "username", "zk_password": "zk_password", - "servers": [ - { - "inner_ip": "127.0.0.1", - "outer_ip": "127.0.0.2" - } - ], + "btfileserver": { + "inner_ips": [{"inner_ip": "127.0.0.1"}], + "outer_ips": [{"outer_ip": "127.0.0.2"}] + }, + "taskserver": { + "inner_ips": [{"inner_ip": "127.0.0.1"}], + "outer_ips": [{"outer_ip": "127.0.0.2"}] + }, + "dataserver": { + "inner_ips": [{"inner_ip": "127.0.0.1"}], + "outer_ips": [{"outer_ip": "127.0.0.2"}] + }, "package_inner_url": "http://127.0.0.1/download/", "package_outer_url": "http://127.0.0.2/download/", "agent_config": { @@ -125,12 +131,18 @@ def retrieve(self, request, *args, **kwargs): ] "zk_user": "username", "zk_password": "zk_password", - "servers": [ - { - "inner_ip": "127.0.0.1", - "outer_ip": "127.0.0.2" - } - ], + "btfileserver": { + "inner_ips": [{"inner_ip": "127.0.0.1"}], + "outer_ips": [{"outer_ip": "127.0.0.2"}] + }, + "taskserver": { + "inner_ips": [{"inner_ip": "127.0.0.1"}], + "outer_ips": [{"outer_ip": "127.0.0.2"}] + }, + "dataserver": { + "inner_ips": [{"inner_ip": "127.0.0.1"}], + "outer_ips": [{"outer_ip": "127.0.0.2"}] + }, "package_inner_url": "http://127.0.0.1/download/", "package_outer_url": "http://127.0.0.2/download/", "agent_config": { @@ -306,12 +318,18 @@ def update(self, request, *args, **kwargs): @apiParamExample {Json} 请求参数 { "name": "接入点名称", - "servers": [ - { - "inner_ip": "127.0.0.1", - "outer_ip": "127.0.0.2" - } - ], + "btfileserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, + "taskserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, + "dataserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, "package_inner_url": "http://127.0.0.1/download/", "package_outer_url": "http://127.0.0.2/download/", "agent_config": { @@ -407,12 +425,18 @@ def test(self, request, *args, **kwargs): @apiParam {String} package_outer_url 安装包外网地址 @apiParamExample {Json} 请求参数 { - "servers": [ - { - "inner_ip": "127.0.0.1", - "outer_ip": "127.0.0.2" - } - ], + "btfileserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, + "taskserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, + "dataserver": { + "inner_ip_infos": [{"ip": "127.0.0.1"}], + "outer_ip_infos": [{"ip": "127.0.0.2"}] + }, "package_inner_url": "http://127.0.0.1/download/", "package_outer_url": "http://127.0.0.2/download/" } diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index b7f44e364..c9d924368 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -20,5 +20,6 @@ module.exports = { rules: { '@typescript-eslint/member-ordering': 'off', 'no-param-reassign': 'off', + 'vue/camelcase': 'off', }, }; diff --git a/frontend/src/common/form-check.ts b/frontend/src/common/form-check.ts index 7ba6d7c9b..7bb4640bd 100644 --- a/frontend/src/common/form-check.ts +++ b/frontend/src/common/form-check.ts @@ -1,3 +1,4 @@ +import i18n from '@/setup'; import { isEmpty } from './util'; import { splitCodeArr, @@ -34,7 +35,7 @@ export function createIpRegu(type: 'IPv4' | 'IPv6' | 'mixins' = 'IPv4', isBatch : (val: string) => !val || regex.test(val); return { trigger: 'blur', - message: window.i18n.t('IP格式不正确'), + message: i18n.t('IP格式不正确'), // regex, validator, }; @@ -45,7 +46,7 @@ export function createIpRegu(type: 'IPv4' | 'IPv6' | 'mixins' = 'IPv4', isBatch */ export const reguRequired = { required: true, - message: window.i18n.t('必填项'), + message: i18n.t('必填项'), trigger: 'blur', }; export const reguIp = createIpRegu(); @@ -57,39 +58,41 @@ export const reguIpMixinsBatch = createIpRegu('mixins', true); export const reguUrl = { regex: regUrl, validator: (val: string) => regUrl.test(val), - message: window.i18n.t('URL格式不正确'), + message: i18n.t('URL格式不正确'), trigger: 'blur', }; export const reguUrlMixinIp = { regex: regUrlMixinIp, - message: window.i18n.t('URL格式不正确'), + message: i18n.t('URL格式不正确'), trigger: 'blur', validator: (val: string) => regUrlMixinIp.test(val), }; export const reguPort = { // validator: (val: string) => regNaturalNumber.test(val) && parseInt(val, 10) <= 65535, // 端口范围不应该包括 - validator: (val: string) => regNaturalNumber.test(val) && val && parseInt(val, 10) && parseInt(val, 10) <= 65535, - message: window.i18n.t('端口范围', { range: '1-65535' }), + validator: (val: string): boolean => regNaturalNumber.test(val) + && val && parseInt(val, 10) + && parseInt(val, 10) <= 65535, + message: i18n.t('端口范围', { range: '1-65535' }), trigger: 'blur', }; export const reguNaturalNumber = { regex: regNaturalNumber, validator: (val: string) => regNaturalNumber.test(val), - message: window.i18n.t('不小于零的整数'), + message: i18n.t('不小于零的整数'), trigger: 'blur', }; export function reguFnName(params?: { max: number } = {}) { const { max = 32 } = params; return { validator: (val: string) => regNormalText.test(val) && regrLengthCheck(val, max), - message: window.i18n.t('正常输入内容校验', [max]), + message: i18n.t('正常输入内容校验', [max]), trigger: 'blur', }; } export function reguFnStrLength(max = 40) { return { validator: (val: string) => regrLengthCheck(val, max), - message: window.i18n.t('字符串长度校验', [Math.floor(max / 2), max]), + message: i18n.t('字符串长度校验', [Math.floor(max / 2), max]), trigger: 'blur', }; } @@ -98,14 +101,14 @@ export function reguFnSysPath(params?: { [key: string]: number | string } = {}) const reg = regFnSysPath({ minText, maxText, minLevel, type }); return { type, - message: window.i18n.t(i18nMap[type], { minLevel, maxText }), + message: i18n.t(i18nMap[type], { minLevel, maxText }), trigger: 'blur', validator: (val: string) => reg.test(val), }; } export function reguFnMinInteger(min = 0) { return { - message: window.i18n.t('整数最小值校验提示', { min }), + message: i18n.t('整数最小值校验提示', { min }), validator: (val: string) => isEmpty(val) || (regInteger.test(val) && parseInt(val, 10) >= min), trigger: 'blur', }; @@ -113,14 +116,14 @@ export function reguFnMinInteger(min = 0) { export function reguFnRangeInteger(min: number, max: number) { return { validator: (val: string) => regInteger.test(val) && Number(val) <= max && Number(val) >= min, - message: window.i18n.t('整数范围校验提示', { max, min }), + message: i18n.t('整数范围校验提示', { max, min }), trigger: 'blur', }; } // 一行内不能重复 export const reguIpInLineRepeat = { trigger: 'blur', - message: window.i18n.t('冲突校验', { prop: 'IP' }), + message: i18n.t('冲突校验', { prop: 'IP' }), validator(val: string) { if (!val) return true; const splitCode = splitCodeArr.find(split => val.indexOf(split) > 0); diff --git a/frontend/src/common/form-label-hook.ts b/frontend/src/common/form-label-hook.ts new file mode 100644 index 000000000..31eb51d59 --- /dev/null +++ b/frontend/src/common/form-label-hook.ts @@ -0,0 +1,34 @@ +import { ref } from 'vue'; + +export default function useFormLabelWidth() { + const minWidth = ref(0); + + const initLabelWidth = (form: Vue) => { + const el = form ? form.$el : null; + if (!el) return; + let max = 0; + const leftPadding = 28; + const safePadding = 8; + const $labelEleList = el.querySelectorAll('.bk-label'); + $labelEleList.forEach((item) => { + const spanEle = item.querySelector('span'); + if (spanEle) { + const { width } = spanEle.getBoundingClientRect(); + max = Math.max(minWidth.value, max, width); + } + }); + const width = Math.ceil(max + leftPadding + safePadding); + $labelEleList.forEach((item) => { + (item as HTMLElement).style.width = `${width}px`; + }); + el.querySelectorAll('.bk-form-content').forEach((item) => { + (item as HTMLElement).style.marginLeft = `${width}px`; + }); + return width; + }; + + return { + minWidth, + initLabelWidth, + }; +} diff --git a/frontend/src/common/regexp.ts b/frontend/src/common/regexp.ts index e6af81536..1ff6b9e6f 100644 --- a/frontend/src/common/regexp.ts +++ b/frontend/src/common/regexp.ts @@ -18,7 +18,7 @@ export const regIp = new RegExp(`^${IpStr}$`); export const regFilterIp = new RegExp(`^(?:\\d+:)?(${IpStr})$`); export const regExclusiveFilterIp = new RegExp(`^\\d+:${IpStr}$`); export const regIPv6 = new RegExp(`^${IPv6Str}$`); -export const regIpMixin = window.$DHCP ? new RegExp(`^${IpStr}|${IPv6Str}$`) : regIp; // 区分环境可用的IP类型 +export const regIpMixin = window.$DHCP ? new RegExp(`^${IpStr}$|^${IPv6Str}$`) : regIp; // 区分环境可用的IP类型 export const regFilterIpMixin = window.$DHCP ? new RegExp(`^(?:\\d+:)?(${IpStr}|${IPv6Str})$`) : regFilterIp; export const regExclusiveFilterIpMixin = window.$DHCP ? new RegExp(`^\\d+:(${IpStr}|${IPv6Str})$`) : regExclusiveFilterIp; // 用于区分IP还是按管控区域筛选ip export const regUrl = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'*+,;=.]+$/; diff --git a/frontend/src/components/RussianDolls/DollForm.vue b/frontend/src/components/RussianDolls/DollForm.vue index 4b1ee7b61..18c41d56f 100644 --- a/frontend/src/components/RussianDolls/DollForm.vue +++ b/frontend/src/components/RussianDolls/DollForm.vue @@ -16,10 +16,22 @@ import { deepClone } from '@/common/util'; export default defineComponent({ name: 'RussianDollsForm', props: { - data: () => [], - layout: () => [], - rules: () => ({}), - value: () => ({}), + data: { + type: Array, + default: () => [], + }, + layout: { + type: Array, + default: () => [], + }, + rules: { + type: Object, + default: () => ({}), + }, + value: { + type: Object, + default: () => ({}), + }, labelWidth: { type: Number, default: 150, diff --git a/frontend/src/components/RussianDolls/item/DollArray.vue b/frontend/src/components/RussianDolls/item/DollArray.vue index 8af324e14..14a94b518 100644 --- a/frontend/src/components/RussianDolls/item/DollArray.vue +++ b/frontend/src/components/RussianDolls/item/DollArray.vue @@ -40,10 +40,22 @@ import bus from '@/common/bus'; export default defineComponent({ props: { - item: () => ({}), - schema: () => ({}), - value: () => [], - valueProp: '', + item: { + type: Object, + default: () => ({}), + }, + schema: { + type: Object, + default: () => ({}), + }, + value: { + type: Array, + default: () => [], + }, + valueProp: { + type: String, + default: '', + }, labelWidth: { type: Number, default: 110, diff --git a/frontend/src/components/RussianDolls/item/DollBase.vue b/frontend/src/components/RussianDolls/item/DollBase.vue index 78b824b1c..3b3a7d2e3 100644 --- a/frontend/src/components/RussianDolls/item/DollBase.vue +++ b/frontend/src/components/RussianDolls/item/DollBase.vue @@ -22,12 +22,30 @@ import { defineComponent, inject, toRefs } from 'vue'; export default defineComponent({ props: { - item: () => ({}), - schema: () => ({}), - itemIndex: -1, - value: '', - valueProp: '', - labelWidth: 110, + item: { + type: Object, + default: () => ({}), + }, + schema: { + type: Object, + default: () => ({}), + }, + itemIndex: { + type: Number, + default: -1, + }, + value: { + type: String, + default: '', + }, + valueProp: { + type: String, + default: '', + }, + labelWidth: { + type: Number, + default: 110, + }, }, setup(props) { const updateFormData = inject('updateFormData'); diff --git a/frontend/src/components/RussianDolls/item/DollIndex.vue b/frontend/src/components/RussianDolls/item/DollIndex.vue index 9543462a0..7d31bf906 100644 --- a/frontend/src/components/RussianDolls/item/DollIndex.vue +++ b/frontend/src/components/RussianDolls/item/DollIndex.vue @@ -38,11 +38,26 @@ import { defineComponent, toRefs } from 'vue'; export default defineComponent({ props: { - item: () => ({}), - itemIndex: -1, - value: () => ({}), - valueProp: '', - labelWidth: 110, + item: { + type: Object, + default: () => ({}), + }, + itemIndex: { + type: Number, + default: -1, + }, + value: { + type: Object, + default: () => ({}), + }, + valueProp: { + type: String, + default: '', + }, + labelWidth: { + type: Number, + default: 110, + }, }, setup(props) { return { diff --git a/frontend/src/components/RussianDolls/item/DollObject.vue b/frontend/src/components/RussianDolls/item/DollObject.vue index 55f3319a1..fa4f3ee8f 100644 --- a/frontend/src/components/RussianDolls/item/DollObject.vue +++ b/frontend/src/components/RussianDolls/item/DollObject.vue @@ -17,11 +17,26 @@ import { defineComponent, inject, toRefs } from 'vue'; export default defineComponent({ props: { - item: () => ({}), - schema: () => ({}), - itemIndex: -1, - value: () => ({}), - valueProp: '', + item: { + type: Object, + default: () => ({}), + }, + schema: { + type: Object, + default: () => ({}), + }, + itemIndex: { + type: Number, + default: -1, + }, + value: { + type: Object, + default: () => ({}), + }, + valueProp: { + type: String, + default: '', + }, labelWidth: { type: Number, default: 110, diff --git a/frontend/src/components/common/strategy-table.vue b/frontend/src/components/common/strategy-table.vue index efcfbafb9..652b65546 100644 --- a/frontend/src/components/common/strategy-table.vue +++ b/frontend/src/components/common/strategy-table.vue @@ -168,7 +168,7 @@ export default class StrategyTable extends Vue { }, { source: 'Agent', - targetAdress: 'GSE_task', + targetAdress: 'GSE_cluster', protocol: 'TCP', portKey: 'io_port', use: this.$t('任务服务端口'), @@ -397,7 +397,7 @@ export default class StrategyTable extends Vue { Proxy: [ { source: 'Proxy(gse_agent)', - targetAdress: 'GSE_task', + targetAdress: 'GSE_cluster', protocol: 'TCP', portKey: 'io_port', use: this.$t('任务服务端口'), diff --git a/frontend/src/components/common/strategy-template.vue b/frontend/src/components/common/strategy-template.vue index 6dd1aa20d..32c0f09f6 100644 --- a/frontend/src/components/common/strategy-template.vue +++ b/frontend/src/components/common/strategy-template.vue @@ -115,7 +115,7 @@ export default class StrategyTemplate extends Vue { let idKey = cloud.ap_id; let ap = AgentStore.apList.find(apItem => apItem.id === idKey) as IApExpand; - const serverKey = cloud.type === 'Agent' ? 'inner_ip' : 'outer_ip'; // Pagent 非必要 + const serverKey = cloud.type === 'Agent' ? 'inner_ip_infos' : 'outer_ip_infos'; // Pagent 非必要 const proxyKey = cloud.type === 'Proxy' ? 'outer_ip' : 'inner_ip'; // Agent 非必要 // 先排除掉找不到接入点的主机 if (ap) { @@ -136,9 +136,9 @@ export default class StrategyTemplate extends Vue { ap_name: ap.name, zk: ap.zk_hosts.map(zk => zk.zk_ip), // 仅Agent zkHosts: ap.zk_hosts, - btfileserver: ap.btfileserver.map(server => server[serverKey]), - dataserver: ap.dataserver.map(server => server[serverKey]), - taskserver: ap.taskserver.map(server => server[serverKey]), + btfileserver: ap.btfileserver[serverKey].map(item => item.ip), + dataserver: ap.btfileserver[serverKey].map(item => item.ip), + taskserver: ap.btfileserver[serverKey].map(item => item.ip), proxy: cloud.proxy.map((item: any) => item[proxyKey]), agent: cloud[proxyKey] ? [cloud[proxyKey]] : [], // Proxy 非必要 ...ap.port_config, diff --git a/frontend/src/config/test-anchor-key.ts b/frontend/src/config/test-anchor-key.ts index 7da3f67f8..d03266282 100644 --- a/frontend/src/config/test-anchor-key.ts +++ b/frontend/src/config/test-anchor-key.ts @@ -196,6 +196,7 @@ export default { accessPoint: { apBaseForm: 'form_apBase', apBaseTable: 'table_apBase', + apOmitBtn: 'btn_apOmit', apTestBtn: 'btn_apTest', apBaseInput: 'input_apBase', addRowBtn: 'btn_addRow', diff --git a/frontend/src/i18n/en.js b/frontend/src/i18n/en.js index fb9326624..0998df36f 100644 --- a/frontend/src/i18n/en.js +++ b/frontend/src/i18n/en.js @@ -472,7 +472,7 @@ export default { Server信息: 'Server information', Server序号: 'Server serial number ', Agent信息: 'Agent information', - 安装包: 'Installation package', + Proxy安装包: 'Proxy installation package', 内网: 'Intranet:', 外网: 'Extranet:', hostid路径: 'Hostid path', @@ -482,10 +482,12 @@ export default { 数据文件路径: 'Data file path', 临时文件路径: 'Temporary file path', 接入点名称: 'Access point', + 接入点版本: 'Access point version', + 接入点描述: 'Select the associated GSE channel, V1 is associated with GSE1.0, V2 is associated with GSE2.0', 用户创建的接入点: 'User-created access point', 请输入Server的内网IP: 'Please enter the intranet IP of {type} Server', 请输入Server的外网IP: 'Please enter the extranet IP of {type} Server', - Agent安装包: 'Agent package', + Agent安装包信息: 'Agent package', Agent包URL: 'Package URL', Agent包服务器目录: 'Package path', 请输入服务器目录: 'Please enter the the server path', @@ -529,6 +531,12 @@ export default { 加载插件基础信息成功: 'Load plugin basic information successfully', 该接入点被使用中无法删除: 'This access point is in use and cannot be deleted', 序号: 'No.', + GSEFile服务地址: 'GSE File Endpoints', + GSEData服务地址: 'GSE Data Endpoints', + GSECluster服务地址: 'GSE Cluster Endpoints', + 跳过检测: 'Skip Detection', + 回调地址: 'Callback Address', + IPv4和IPv6不能混合使用: 'IPv4 and IPv6 cannot be mixed', // 自监控 后台服务器: 'Servers', @@ -567,6 +575,8 @@ export default { 内网回调: 'LAN callback', 请输入内网回调地址: 'Please enter the LAN callback address', 请输入以backend结尾的URL地址: 'Please enter the URL address ending with /backend', + 内网URL: 'LAN url', + 外网URL: 'WAN url', // 任务历史 机器数量: '(total {0})', diff --git a/frontend/src/i18n/zh.js b/frontend/src/i18n/zh.js index 6a1b0f2b0..4c96ceb1d 100644 --- a/frontend/src/i18n/zh.js +++ b/frontend/src/i18n/zh.js @@ -374,9 +374,9 @@ export default { 主机属性: '主机属性', 登录信息: '登录信息', 批量应用: '批量应用', - 内网IP: '内网 IP', - 内网IPv4: '内网 IPv4', - 内网IPv6: '内网 IPv6', + 内网IP: '内网IP', + 内网IPv4: '内网IPv4', + 内网IPv6: '内网IPv6', '「登录IP」': '「登录 IP」', '「内网IPv4」': '「内网IPv4」', '「内网IPv6」': '「内网IPv6」', @@ -472,7 +472,7 @@ export default { Server信息: 'Server信息', Server序号: 'Server序号', Agent信息: 'Agent信息', - 安装包: '安装包', + Proxy安装包: 'Proxy安装包', 内网: '内网: ', 外网: '外网: ', hostid路径: 'hostid路径', @@ -482,10 +482,12 @@ export default { 数据文件路径: '数据文件路径', 临时文件路径: '临时文件路径', 接入点名称: '接入点名称', + 接入点版本: '接入点版本', + 接入点描述: '选择关联的GSE通道,V1关联GSE1.0,V2关联GSE2.0', 用户创建的接入点: '用户创建的接入点', 请输入Server的内网IP: '请输入{type} Server的内网IP', 请输入Server的外网IP: '请输入{type} Server的外网IP', - Agent安装包: 'Agent安装包', + Agent安装包信息: 'Agent安装包信息', Agent包URL: 'Agent包URL', Agent包服务器目录: 'Agent包服务器目录', 请输入服务器目录: '请输入服务器目录', @@ -529,6 +531,12 @@ export default { 加载插件基础信息成功: '加载插件基础信息成功', 该接入点被使用中无法删除: '该接入点被使用中,无法删除', 序号: '序号', + GSEFile服务地址: 'GSE File地址', + GSEData服务地址: 'GSE Data地址', + GSECluster服务地址: 'GSE Cluster地址', + 跳过检测: '跳过检测', + 回调地址: '回调地址', + IPv4和IPv6不能混合使用: 'IPv4和IPv6不能混合使用', // 自监控 后台服务器: '后台服务器', @@ -567,6 +575,8 @@ export default { 内网回调: '内网回调', 请输入内网回调地址: '请输入内网回调地址', 请输入以backend结尾的URL地址: '请输入以/backend结尾的URL地址', + 内网URL: '内网URL', + 外网URL: '外网URL', // 任务历史 机器数量: '(共 {0} 个)', diff --git a/frontend/src/setup/i18n-setup.ts b/frontend/src/setup/i18n-setup.ts index 8f31b9800..27bd78c67 100644 --- a/frontend/src/setup/i18n-setup.ts +++ b/frontend/src/setup/i18n-setup.ts @@ -71,3 +71,5 @@ export function loadLanguageAsync(lang: string) { } window.i18n = i18n; + +export default i18n; diff --git a/frontend/src/store/modules/config.ts b/frontend/src/store/modules/config.ts index 8b0346edb..afe07ceb0 100644 --- a/frontend/src/store/modules/config.ts +++ b/frontend/src/store/modules/config.ts @@ -7,7 +7,7 @@ import { apIsUsing, listAp, retrieveAp, createAp, updateAp, testAp, deleteAp } f import { retrieveGlobalSettings, jobSettings } from '@/api/modules/meta'; import { listApPermission } from '@/api/modules/permission'; import { healthz } from '@/api/modules/healthz'; -import { initIpProp } from '@/setup/ipv6'; +// import { initIpProp } from '@/setup/ipv6'; // eslint-disable-next-line new-cap @Module({ name: 'config', namespaced: true }) @@ -39,15 +39,18 @@ export default class ConfigStore extends VuexModule { zk_hosts: [ { zk_ip: '', zk_port: '' }, ], - btfileserver: [ - { inner_ip: '', outer_ip: '' }, - ], - dataserver: [ - { inner_ip: '', outer_ip: '' }, - ], - taskserver: [ - { inner_ip: '', outer_ip: '' }, - ], + btfileserver: { + inner_ip_infos: [{ ip: '' }], + outer_ip_infos: [{ ip: '' }], + }, + dataserver: { + inner_ip_infos: [{ ip: '' }], + outer_ip_infos: [{ ip: '' }], + }, + taskserver: { + inner_ip_infos: [{ ip: '' }], + outer_ip_infos: [{ ip: '' }], + }, callback_url: '', outer_callback_url: '', package_inner_url: '', @@ -68,11 +71,12 @@ export default class ConfigStore extends VuexModule { // this.apDetail[`${key}`] = detail[key] // } else { if (['btfileserver', 'dataserver', 'taskserver'].includes(key)) { - Vue.set(this.apDetail, key, detail[key].map((server) => { - const copyServer = { ...server }; - initIpProp(copyServer, ['inner_ip', 'outer_ip']); - return copyServer; - })); + Vue.set(this.apDetail, key, { ...detail[key] }); + // Vue.set(this.apDetail, key, detail[key].map((server) => { + // const copyServer = { ...server }; + // initIpProp(copyServer, ['inner_ip', 'outer_ip']); + // return copyServer; + // })); } else { Vue.set(this.apDetail, key, detail[key]); } diff --git a/frontend/src/types/config/config.ts b/frontend/src/types/config/config.ts index 204127bd5..77d6c29ed 100644 --- a/frontend/src/types/config/config.ts +++ b/frontend/src/types/config/config.ts @@ -1,10 +1,16 @@ /* eslint-disable camelcase */ +export type IServerProp = 'inner_ip_infos' | 'outer_ip_infos'; export interface IIpGroup { - inner_ip: string - outer_ip: string - inner_ipv6?: string - outer_ipv6?: string + inner_ip_infos: { ip: string }[] + outer_ip_infos: { ip: string }[] + +// inner_ip: string +// outer_ip: string +// inner_ipv6?: string +// outer_ipv6?: string } + + export interface IZk { zk_ip: string zk_port: string @@ -36,16 +42,16 @@ export interface IApBase { zk_hosts: IZk[] city_id: string region_id: string - btfileserver: IIpGroup[] - dataserver: IIpGroup[] - taskserver: IIpGroup[] + btfileserver: IIpGroup + dataserver: IIpGroup + taskserver: IIpGroup description: string callback_url: string outer_callback_url: string package_inner_url: string package_outer_url: string nginx_path: null | '' - gse_version: 'V1' | 'V2' // ENABLE_AP_VERSION_MUTEX 开启时使用 + gse_version?: 'V1' | 'V2' // ENABLE_AP_VERSION_MUTEX 开启时使用 } export interface IApParams extends IApBase { @@ -66,12 +72,15 @@ export interface IAp extends IApParams { } export interface IApExpand extends IAp { - BtfileServer: IIpGroup[] - DataServer: IIpGroup[] - TaskServer: IIpGroup[] - is_used?: boolean - // is_default?: boolean - collapse: boolean + BtfileServer: IIpGroup; + DataServer: IIpGroup; + TaskServer: IIpGroup; + is_used?: boolean; + // is_default?: boolean; + collapse: boolean; + linux: { name: TranslateResult; value: string; }[]; + windows: { name: TranslateResult; value: string; }[]; + view?: boolean; } export interface ITaskConfig { @@ -86,9 +95,9 @@ export interface ITaskConfig { // server 可用性 export interface IAvailable { - btfileserver: IIpGroup[] - dataserver: IIpGroup[] - taskserver: IIpGroup[] + btfileserver: IIpGroup + dataserver: IIpGroup + taskserver: IIpGroup package_inner_url: string package_outer_url: string callback_url?: string diff --git a/frontend/src/views/cloud/cloud-channel/channel-edit.vue b/frontend/src/views/cloud/cloud-channel/channel-edit.vue index 3d77adf84..c8bfacc36 100644 --- a/frontend/src/views/cloud/cloud-channel/channel-edit.vue +++ b/frontend/src/views/cloud/cloud-channel/channel-edit.vue @@ -112,9 +112,9 @@ export default class ChannelEdit extends Vue { private btnLoading = false; private channelForm: Dictionary = {}; private labelMap: Dictionary = { - btfileserver: 'Btfileserver', - dataserver: 'Dataserver', - taskserver: 'Taskserver', + btfileserver: this.$t('GSE File服务地址'), + dataserver: this.$t('GSE Data服务地址'), + taskserver: this.$t('GSE Cluster服务地址'), }; private showAdvancedConfig = false; private edited = false; diff --git a/frontend/src/views/cloud/cloud-channel/channel-table.vue b/frontend/src/views/cloud/cloud-channel/channel-table.vue index 851851562..46057d779 100644 --- a/frontend/src/views/cloud/cloud-channel/channel-table.vue +++ b/frontend/src/views/cloud/cloud-channel/channel-table.vue @@ -42,13 +42,19 @@ import { CloudStore } from '@/store'; export default class ChannelTable extends Vue { @Prop({ default: () => ({}), type: Object }) private readonly channel!: Dictionary; + private label: Dictionary = { + btfileserver: this.$t('GSE File服务地址'), + dataserver: this.$t('GSE Data服务地址'), + taskserver: this.$t('GSE Cluster服务地址'), + }; + private get channelServerKeys() { return CloudStore.channelServerKeys; } private get serverList() { const { upstream_servers: servers = {} } = this.channel; return this.channelServerKeys.map(key => ({ - key, + key: this.label[key], value: servers[key] ? servers[key].join(';') : '', })); } diff --git a/frontend/src/views/global-config/gse-config/access-point-table.vue b/frontend/src/views/global-config/gse-config/access-point-table.vue index fb86bbff6..86d5e2dc0 100644 --- a/frontend/src/views/global-config/gse-config/access-point-table.vue +++ b/frontend/src/views/global-config/gse-config/access-point-table.vue @@ -1,10 +1,9 @@ -

+

ID: {{ accessPoint.id | filterEmpty }} {{ accessPoint.description }} + {{ accessPoint.gse_version }}

>> .access-point-table tr:last-child td { - border-bottom: 0; - } } diff --git a/frontend/src/views/global-config/gse-config/set-access-point/apFormConfig.ts b/frontend/src/views/global-config/gse-config/set-access-point/apFormConfig.ts index 4382a25db..712ecd799 100644 --- a/frontend/src/views/global-config/gse-config/set-access-point/apFormConfig.ts +++ b/frontend/src/views/global-config/gse-config/set-access-point/apFormConfig.ts @@ -8,6 +8,24 @@ export const stepHost = [ ruleName: 'name', placeholder: window.i18n.t('用户创建的接入点'), }, + { + label: window.i18n.t('接入点版本'), + key: 'gse_version', + required: true, + ruleName: 'gse_version', + desc: window.i18n.t('接入点描述'), + type: 'select', + options: [ + { + id: 'V1', + name: 'V1' + }, + { + id: 'V2', + name: 'V2' + }, + ], + }, { label: window.i18n.t('接入点说明'), key: 'description', @@ -45,42 +63,56 @@ export const stepHost = [ type: 'zk', }, { - label: window.i18n.t('外网回调地址'), - key: 'outer_callback_url', - ruleName: 'callback', - placeholder: window.i18n.t('请输入外网回调地址'), - extCls: 'mt20', + type: 'url', + items: [ + { + label: window.i18n.t('回调地址'), + prepend: window.i18n.t('内网URL'), + key: 'callback_url', + ruleName: 'callback', + placeholder: window.i18n.t('请输入内网回调地址'), + extCls: 'mt20', + }, + { + label: '', + prepend: window.i18n.t('外网URL'), + key: 'outer_callback_url', + ruleName: 'callback', + placeholder: window.i18n.t('请输入外网回调地址'), + extCls: 'mt10', + }, + ], }, { - label: window.i18n.t('内网回调地址'), - key: 'callback_url', - ruleName: 'callback', - placeholder: window.i18n.t('请输入内网回调地址'), - extCls: 'mt20', + type: 'url', + items: [ + { + label: window.i18n.t('Agent安装包地址'), + prepend: window.i18n.t('内网URL'), + required: true, + key: 'package_inner_url', + ruleName: 'url', + placeholder: window.i18n.t('请输入内网下载URL'), + extCls: 'mt20', + }, + { + label: '', + prepend: window.i18n.t('外网URL'), + required: true, + key: 'package_outer_url', + ruleName: 'url', + placeholder: window.i18n.t('请输入外网下载URL'), + extCls: 'mt10 hide-require', + }, + ], }, { label: window.i18n.t('Agent包服务器目录'), key: 'nginx_path', ruleName: 'nginxPath', placeholder: window.i18n.t('请输入服务器目录'), - extCls: 'mt40', - }, - { - label: window.i18n.t('Agent包URL'), - required: true, - key: 'package_inner_url', - ruleName: 'url', - placeholder: window.i18n.t('请输入内网下载URL'), extCls: 'mt20', }, - { - label: '', - required: true, - key: 'package_outer_url', - ruleName: 'url', - placeholder: window.i18n.t('请输入外网下载URL'), - extCls: 'mt10', - }, // 可用性测试 { type: 'usability', @@ -117,12 +149,12 @@ export const apAgentInfo = [ ]; // 目录名可以包含但不相等,所以末尾加了 /, 校验的时候给值也需要加上 / -const linuxNotInclude = [ +export const linuxNotInclude = [ '/etc/', '/root/', '/boot/', '/dev/', '/sys/', '/tmp/', '/var/', '/usr/lib/', '/usr/lib64/', '/usr/include/', '/usr/local/etc/', '/usr/local/sa/', '/usr/local/lib/', '/usr/local/lib64/', '/usr/local/bin/', '/usr/local/libexec/', '/usr/local/sbin/', ]; -const linuxNotIncludeError = [ +export const linuxNotIncludeError = [ '/etc', '/root', '/boot', '/dev', '/sys', '/tmp', '/var', '/usr/lib', '/usr/lib64', '/usr/include', '/usr/local/etc', '/usr/local/sa', '/usr/local/lib', '/usr/local/lib64', '/usr/local/bin', '/usr/local/libexec', '/usr/local/sbin', diff --git a/frontend/src/views/global-config/gse-config/set-access-point/host-td-input.vue b/frontend/src/views/global-config/gse-config/set-access-point/host-td-input.vue new file mode 100644 index 000000000..640416977 --- /dev/null +++ b/frontend/src/views/global-config/gse-config/set-access-point/host-td-input.vue @@ -0,0 +1,70 @@ + + + diff --git a/frontend/src/views/global-config/gse-config/set-access-point/host-td-operate.vue b/frontend/src/views/global-config/gse-config/set-access-point/host-td-operate.vue new file mode 100644 index 000000000..f1eb402cd --- /dev/null +++ b/frontend/src/views/global-config/gse-config/set-access-point/host-td-operate.vue @@ -0,0 +1,61 @@ + + + + diff --git a/frontend/src/views/global-config/gse-config/set-access-point/step-form-table.vue b/frontend/src/views/global-config/gse-config/set-access-point/step-form-table.vue index b8320dd3e..b9d28b9f5 100644 --- a/frontend/src/views/global-config/gse-config/set-access-point/step-form-table.vue +++ b/frontend/src/views/global-config/gse-config/set-access-point/step-form-table.vue @@ -76,7 +76,7 @@ export default class SetupFormTable extends Vue { text-align: left; font-weight: 400; &:first-child { - padding-left: 16px; + text-align: center; } } } diff --git a/frontend/src/views/global-config/gse-config/set-access-point/step-host.vue b/frontend/src/views/global-config/gse-config/set-access-point/step-host.vue index 44db476ca..bdea2b559 100644 --- a/frontend/src/views/global-config/gse-config/set-access-point/step-host.vue +++ b/frontend/src/views/global-config/gse-config/set-access-point/step-host.vue @@ -1,56 +1,96 @@