From 5cf19cb7072999915f34c23df444975727e695a7 Mon Sep 17 00:00:00 2001 From: xcwang <1366993017@qq.com> Date: Mon, 6 Mar 2023 10:05:40 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=20=E6=8F=92=E4=BB=B6=E8=B0=83?= =?UTF-8?q?=E8=AF=95=E6=94=AF=E6=8C=81=E6=8C=89=E6=9C=8D=E5=8A=A1=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E7=BB=B4=E5=BA=A6=E8=BF=9B=E8=A1=8C=E8=B0=83=E8=AF=95?= =?UTF-8?q?=20(closed=20#1344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/plugin/serializers.py | 24 +++++- apps/backend/plugin/views.py | 82 ++++++++++++------- apps/backend/tests/api/test_debug.py | 117 +++++++++++++++++++++++++++ apps/node_man/exceptions.py | 6 ++ 4 files changed, 197 insertions(+), 32 deletions(-) create mode 100644 apps/backend/tests/api/test_debug.py diff --git a/apps/backend/plugin/serializers.py b/apps/backend/plugin/serializers.py index 158c72f99b..0770206328 100644 --- a/apps/backend/plugin/serializers.py +++ b/apps/backend/plugin/serializers.py @@ -17,7 +17,7 @@ from rest_framework import serializers from apps.exceptions import BackendValidationError, ValidationError -from apps.node_man import constants +from apps.node_man import constants, models from apps.node_man.models import DownloadRecord, GsePluginDesc, Packages @@ -250,17 +250,35 @@ def validate(self, attrs): return attrs raise ValidationError("`bk_host_id` or `ip + bk_cloud_id (Only valid for static host)` required") + class InstanceInfoSerializer(serializers.Serializer): + bk_biz_id = serializers.IntegerField(required=True) + id = serializers.IntegerField(required=False) + bk_inst_id = serializers.CharField(required=False) + bk_obj_id = serializers.CharField(required=False) + plugin_id = serializers.IntegerField(required=False) plugin_name = serializers.CharField(max_length=32, required=False) version = serializers.CharField(max_length=32, required=False) config_ids = serializers.ListField(default=[], allow_empty=True, child=serializers.IntegerField()) - host_info = HostInfoSerializer(required=True) + object_type = serializers.ChoiceField( + choices=models.Subscription.OBJECT_TYPE_CHOICES, label="对象类型", default=models.Subscription.ObjectType.HOST + ) + node_type = serializers.ChoiceField( + choices=models.Subscription.NODE_TYPE_CHOICES, label="节点类别", default=models.Subscription.NodeType.INSTANCE + ) + host_info = HostInfoSerializer(required=False) + instance_info = InstanceInfoSerializer(required=False) def validate(self, attrs): # 两种参数模式少要有一种满足 - if "id" not in attrs and not ("plugin_name" in attrs and "version") in attrs: + if "plugin_id" not in attrs and not ("plugin_name" in attrs and "version") in attrs: raise ValidationError("`plugin_id` or `plugin_name + version` required") + if "host_info" in attrs and "instance_info" in attrs: + raise ValidationError("`host_info` and `instance_info` cannot be use at the same time") + + if "host_info" not in attrs and "instance_info" not in attrs: + raise ValidationError("`host_info` and `instance_info` must choose one") return attrs diff --git a/apps/backend/plugin/views.py b/apps/backend/plugin/views.py index b71586cdfe..7913205e31 100644 --- a/apps/backend/plugin/views.py +++ b/apps/backend/plugin/views.py @@ -18,6 +18,7 @@ import os import re import shutil +from typing import Any, Dict, List, Optional, Union import six from blueapps.account.decorators import login_exempt @@ -45,12 +46,13 @@ ) from apps.backend.subscription.handler import SubscriptionHandler from apps.backend.subscription.tasks import run_subscription_task_and_create_instance +from apps.backend.subscription.tools import get_service_instance_by_ids from apps.core.files import core_files_constants from apps.core.files.storage import get_storage from apps.exceptions import AppBaseException, ValidationError from apps.generic import APIViewSet from apps.node_man import constants, models -from apps.node_man.exceptions import HostNotExists +from apps.node_man.exceptions import HostNotExists, ServiceInstanceNotFoundError from pipeline.engine.exceptions import InvalidOperationException from pipeline.service import task_service from pipeline.service.pipeline_engine_adapter.adapter_api import STATE_MAP @@ -546,36 +548,58 @@ def start_debug(self, request): @apiGroup backend_plugin """ params = self.validated_data - host_info = params["host_info"] - plugin_name = params["plugin_name"] - plugin_version = params["version"] - if host_info.get("bk_host_id"): - query_host_params = {"bk_biz_id": host_info["bk_biz_id"], "bk_host_id": host_info["bk_host_id"]} + plugin_version: Optional[str] = params.get("version") + object_type: str = params["object_type"] + node_type: str = params["node_type"] + + host_info: Optional[Dict[str, Union[int, str]]] = params.get("host_info") + instance_info: Optional[Dict[str, Union[int, str]]] = params.get("instance_info") + + if host_info: + if host_info.get("bk_host_id"): + query_host_params: Dict[str, Union[str, int]] = { + "bk_biz_id": host_info["bk_biz_id"], + "bk_host_id": host_info["bk_host_id"], + } + else: + # 仅支持静态寻址的主机使用 云区域 + IP + query_host_params: Dict[str, Union[str, int]] = { + "bk_biz_id": host_info["bk_biz_id"], + "inner_ip": host_info["ip"], + "bk_cloud_id": host_info["bk_cloud_id"], + "bk_addressing": constants.CmdbAddressingType.STATIC.value, + } + bk_biz_id: int = host_info["bk_biz_id"] + node: Dict[str, Union[int, str]] = host_info else: - # 仅支持静态寻址的主机使用 云区域 + IP - query_host_params = { - "bk_biz_id": host_info["bk_biz_id"], - "inner_ip": host_info["ip"], - "bk_cloud_id": host_info["bk_cloud_id"], - "bk_addressing": constants.CmdbAddressingType.STATIC.value, - } + bk_biz_id: int = instance_info["bk_biz_id"] + service_instance_id: Optional[int] = instance_info["id"] + service_instance_result: List[Dict[str, Any]] = get_service_instance_by_ids( + bk_biz_id=instance_info["bk_biz_id"], ids=[service_instance_id] + ) + try: + bk_host_id: int = service_instance_result[0]["bk_host_id"] + except Exception: + raise ServiceInstanceNotFoundError(id=service_instance_id) + query_host_params: Dict[str, int] = {"bk_biz_id": bk_biz_id, "bk_host_id": bk_host_id} + node: Dict[str, int] = {"id": service_instance_id} try: - host = models.Host.objects.get(**query_host_params) + host: models.Host = models.Host.objects.get(**query_host_params) except models.Host.DoesNotExist: raise HostNotExists("host does not exist") - plugin_id = params.get("plugin_id") + plugin_id: Optional[int] = params.get("plugin_id") if plugin_id: try: - package = models.Packages.objects.get(id=plugin_id) + package: Optional[int] = models.Packages.objects.get(id=plugin_id) except models.Packages.DoesNotExist: raise exceptions.PluginNotExistError() else: - os_type = host.os_type.lower() - cpu_arch = host.cpu_arch + os_type: str = host.os_type.lower() + cpu_arch: str = host.cpu_arch try: - package = models.Packages.objects.get( + package: models.Packages = models.Packages.objects.get( project=params["plugin_name"], version=params["version"], os=os_type, cpu_arch=cpu_arch ) except models.Packages.DoesNotExist: @@ -586,10 +610,10 @@ def start_debug(self, request): if not package.is_ready: raise ValidationError("plugin is not ready") - configs = models.PluginConfigInstance.objects.in_bulk(params["config_ids"]) + configs: Dict[str, Any] = models.PluginConfigInstance.objects.in_bulk(params["config_ids"]) # 渲染配置文件 - step_config_templates = [] + step_config_templates: List[Dict[str, str]] = [] step_params_context = {} for config_id in params["config_ids"]: config = configs.get(config_id) @@ -603,11 +627,11 @@ def start_debug(self, request): step_params_context.update(json.loads(config.render_data)) with transaction.atomic(): - subscription = models.Subscription.objects.create( - bk_biz_id=host_info["bk_biz_id"], - object_type=models.Subscription.ObjectType.HOST, - node_type=models.Subscription.NodeType.INSTANCE, - nodes=[host_info], + subscription: models.Subscription = models.Subscription.objects.create( + bk_biz_id=bk_biz_id, + object_type=object_type, + node_type=node_type, + nodes=[node], enable=False, is_main=params.get("is_main", False), creator=request.user.username, @@ -617,17 +641,17 @@ def start_debug(self, request): # 创建订阅步骤 models.SubscriptionStep.objects.create( subscription_id=subscription.id, - step_id=plugin_name, + step_id=package.project, type="PLUGIN", config={ "config_templates": step_config_templates, "plugin_version": plugin_version, - "plugin_name": plugin_name, + "plugin_name": package.project, "job_type": "DEBUG_PLUGIN", }, params={"context": step_params_context}, ) - subscription_task = models.SubscriptionTask.objects.create( + subscription_task: models.SubscriptionTask = models.SubscriptionTask.objects.create( subscription_id=subscription.id, scope=subscription.scope, actions={} ) diff --git a/apps/backend/tests/api/test_debug.py b/apps/backend/tests/api/test_debug.py new file mode 100644 index 0000000000..cee9d27a71 --- /dev/null +++ b/apps/backend/tests/api/test_debug.py @@ -0,0 +1,117 @@ +# -*- 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 + +import mock + +from apps.backend.tests.components.collections.plugin import utils +from apps.node_man import models +from apps.utils.unittest.testcase import CustomAPITestCase + + +class DebugPluginTestCase(CustomAPITestCase, utils.PluginTestObjFactory): + + INSTANCE_ID = 1 + + def setUp(self): + self.ids: typing.Dict[str, int] = self.init_db() + self.plugin_id: int = models.GsePluginDesc.objects.get(name=utils.PKG_PROJECT_NAME).id + self.start_debug_path: str = "/backend/api/plugin/start_debug/" + self.host_obj: models.Host = models.Host.objects.get(bk_host_id=self.ids["bk_host_id"]) + + service_instance_result: typing.List[typing.Dict[str, typing.Union[str, int]]] = [ + { + "bk_biz_id": utils.DEFAULT_BIZ_ID_NAME["bk_biz_id"], + "id": self.INSTANCE_ID, + "name": "127.0.0.1_bash", + "labels": "", + "bk_host_id": self.host_obj.bk_host_id, + "creator": "admin", + "modifier": "admin", + "create_time": "2022-12-15T09:57:56.764Z", + "last_time": "2022-12-15T09:57:56.764Z", + "bk_supplier_account": "0", + } + ] + + mock.patch( + "apps.backend.plugin.views.get_service_instance_by_ids", return_value=service_instance_result + ).start() + + def test_host_info_debug(self): + base_host_info_data = { + "bk_username": "admin", + "bk_app_code": "bk_nodeman", + "plugin_id": self.plugin_id, + "config_ids": [], + "object_type": models.Subscription.ObjectType.HOST, + "node_type": models.Subscription.NodeType.INSTANCE, + } + host_id_info: typing.Dict[str, typing.Dict[str, typing.Union[str, int]]] = { + "host_info": { + "bk_host_id": self.ids["bk_host_id"], + "bk_supplier_id": 0, + "bk_biz_id": utils.DEFAULT_BIZ_ID_NAME["bk_biz_id"], + } + } + + host_id_debug_result: typing.Dict[str, str] = self.client.post( + path=self.start_debug_path, + data={**base_host_info_data, **host_id_info}, + ) + + without_host_id_info: typing.Dict[str, typing.Dict[str, typing.Union[str, int]]] = { + "host_info": { + "ip": self.host_obj.inner_ip, + "bk_cloud_id": self.host_obj.bk_cloud_id, + "bk_supplier_id": 0, + "bk_biz_id": utils.DEFAULT_BIZ_ID_NAME["bk_biz_id"], + } + } + + without_host_id_debug_result: typing.Dict[str, str] = self.client.post( + path=self.start_debug_path, + data={**base_host_info_data, **without_host_id_info}, + ) + + self.assertTrue(host_id_debug_result["result"]) + self.assertTrue(without_host_id_debug_result["result"]) + + def test_instance_info_debug(self): + base_instance_info_data = { + "bk_username": "admin", + "bk_app_code": "bk_nodeman", + "plugin_id": self.plugin_id, + "config_ids": [], + "object_type": models.Subscription.ObjectType.SERVICE, + "node_type": models.Subscription.NodeType.INSTANCE, + } + + plugin_id_info: typing.Dict[str, int] = {"plugin_id": self.plugin_id} + + instance_info: typing.Dict[str, typing.Dict[str, typing.Union[str, int]]] = { + "instance_info": {"bk_biz_id": utils.DEFAULT_BIZ_ID_NAME["bk_biz_id"], "id": self.INSTANCE_ID} + } + + instance_debug_result: typing.Dict[str, str] = self.client.post( + path=self.start_debug_path, data={**base_instance_info_data, **instance_info, **plugin_id_info} + ) + + plugin_info: typing.Dict[str, str] = { + "plugin_name": utils.PKG_PROJECT_NAME, + "version": utils.PKG_INFO["version"], + } + + instance_debug_without_plugin_id_result: typing.Dict[str, str] = self.client.post( + path=self.start_debug_path, data={**base_instance_info_data, **instance_info, **plugin_info} + ) + self.assertTrue(instance_debug_result["result"]) + self.assertTrue(instance_debug_without_plugin_id_result["result"]) diff --git a/apps/node_man/exceptions.py b/apps/node_man/exceptions.py index 59336b5897..baed34f865 100644 --- a/apps/node_man/exceptions.py +++ b/apps/node_man/exceptions.py @@ -208,3 +208,9 @@ class PluginResourcePolicyNoDiff(NodeManBaseException): class RemoteHostNotExistsError(NodeManBaseException): MESSAGE = _("远程采集主机不存在") ERROR_CODE = 41 + + +class ServiceInstanceNotFoundError(NodeManBaseException): + MESSAGE = _("服务实例不存在") + MESSAGE_TPL = _("服务实例 -> [{id}] 不存在") + ERROR_CODE = 42