diff --git a/apps/backend/components/collections/plugin.py b/apps/backend/components/collections/plugin.py index db4aa1b52..40763de3a 100644 --- a/apps/backend/components/collections/plugin.py +++ b/apps/backend/components/collections/plugin.py @@ -203,7 +203,7 @@ def get_package_by_process_status( """通过进程状态得到插件包对象""" host = self.get_host_by_process_status(process_status, common_data) policy_step_adapter = common_data.policy_step_adapter - package = policy_step_adapter.get_matching_package_obj(host.os_type, host.cpu_arch) + package = policy_step_adapter.get_matching_package_obj(host.os_type, host.cpu_arch, host.bk_biz_id) return package def get_plugin_root_by_process_status( @@ -280,11 +280,12 @@ def _execute(self, data, parent_data, common_data: PluginCommonData): # target_host_objs 的长度通常为1或2,此处也不必担心时间复杂度问题 # 指定 target_host 主要用于远程采集的场景,常见于第三方插件,如拨测 for host in target_host_objs: + bk_biz_id = host.bk_biz_id bk_host_id = host.bk_host_id os_type = host.os_type.lower() cpu_arch = host.cpu_arch group_id = create_group_id(subscription, subscription_instance.instance_info) - package = self.get_package(subscription_instance, policy_step_adapter, os_type, cpu_arch) + package = self.get_package(subscription_instance, policy_step_adapter, os_type, cpu_arch, bk_biz_id) ap_config = self.get_ap_config(ap_id_obj_map, host) setup_path, pid_path, log_path, data_path = self.get_plugins_paths( package, plugin_name, ap_config, group_id, subscription @@ -340,10 +341,11 @@ def get_package( policy_step_adapter: PolicyStepAdapter, os_type: str, cpu_arch: str, + bk_biz_id: int, ) -> models.Packages: """获取插件包对象""" try: - return policy_step_adapter.get_matching_package_obj(os_type, cpu_arch) + return policy_step_adapter.get_matching_package_obj(os_type, cpu_arch, bk_biz_id) except errors.PackageNotExists as error: # 插件包不支持或不存在时,记录异常信息,此实例不参与后续流程 self.move_insts_to_failed([subscription_instance.id], str(error)) @@ -979,7 +981,9 @@ def _execute(self, data, parent_data, common_data: PluginCommonData): # 根据配置模板和上下文变量渲染配置文件 rendered_configs = render_config_files_by_config_templates( - policy_step_adapter.get_matching_config_tmpl_objs(target_host.os_type, target_host.cpu_arch), + policy_step_adapter.get_matching_config_tmpl_objs( + target_host.os_type, target_host.cpu_arch, package, subscription_step.config + ), {"group_id": process_status.group_id}, context, package_obj=package, diff --git a/apps/backend/subscription/steps/adapter.py b/apps/backend/subscription/steps/adapter.py index 56e397578..2dc93241c 100644 --- a/apps/backend/subscription/steps/adapter.py +++ b/apps/backend/subscription/steps/adapter.py @@ -16,6 +16,7 @@ from django.db.models import Max, Subquery, Value from django.utils.translation import ugettext as _ +from packaging import version from rest_framework import exceptions, serializers from apps.backend.subscription import errors @@ -444,27 +445,117 @@ def get_matching_step_params(self, os_type: str = None, cpu_arch: str = None, os return self.os_key_params_map.get(os_key) return self.os_key_params_map.get(self.get_os_key(os_type, cpu_arch), {}) - def get_matching_package_obj(self, os_type: str, cpu_arch: str) -> models.Packages: + def get_matching_package_obj(self, os_type: str, cpu_arch: str, bk_biz_id: int) -> models.Packages: try: package = self.os_key_pkg_map[self.get_os_key(os_type, cpu_arch)] except KeyError: - msg = _("插件 [{name}] 不支持 系统:{os_type}-架构:{cpu_arch}-版本:{plugin_version}").format( - name=self.plugin_name, - os_type=os_type, - cpu_arch=cpu_arch, - plugin_version=self.get_matching_package_dict(os_type, cpu_arch)["version"], + # 如果不存在某个系统架构的版本,则获取最大id的版本 + package = ( + models.Packages.objects.filter(project=self.plugin_name, os=os_type, cpu_arch=cpu_arch) + .order_by("-id") + .first() ) - raise errors.PackageNotExists(msg) - else: - if not package.is_ready: - msg = _("插件 [{name}] 系统:{os_type}-架构:{cpu_arch}-版本:{plugin_version} 未启用").format( + if not package: + msg = _("插件 [{name}] 不支持 系统:{os_type}-架构:{cpu_arch}-版本:{plugin_version}").format( name=self.plugin_name, os_type=os_type, cpu_arch=cpu_arch, plugin_version=self.get_matching_package_dict(os_type, cpu_arch)["version"], ) - raise errors.PluginValidationError(msg) - return package + raise errors.PackageNotExists(msg) - def get_matching_config_tmpl_objs(self, os_type: str, cpu_arch: str) -> List[models.PluginConfigTemplate]: + if not package.is_ready: + msg = _("插件 [{name}] 系统:{os_type}-架构:{cpu_arch}-版本:{plugin_version} 未启用").format( + name=self.plugin_name, + os_type=os_type, + cpu_arch=cpu_arch, + plugin_version=self.get_matching_package_dict(os_type, cpu_arch)["version"], + ) + raise errors.PluginValidationError(msg) + + if len(self.selected_pkg_infos) > 1: + package = self.check_biz_version(package, bk_biz_id) + return package + + def get_matching_config_tmpl_objs( + self, os_type: str, cpu_arch: str, package: models.Packages = None, config: Dict = None + ) -> List[models.PluginConfigTemplate]: + """如果 package 是重新获取的(包括业务锁定版本和tag不存在的版本两种情况),则重新从数据库中获取配置模板""" + if not self.is_pkg_in_selected_pkg(package, self.selected_pkg_infos): + plugin_config_templates = [] + for config_template in config["config_templates"]: + config_tmpl = ( + models.PluginConfigTemplate.objects.filter( + name=config_template["name"], + plugin_name=package.project, + plugin_version=package.version, + is_main=Value(1 if config_template["is_main"] else 0), + ) + .order_by("-id") + .first() + ) + plugin_config_templates.append(config_tmpl) + return plugin_config_templates return self.config_tmpl_obj_gby_os_key.get(self.get_os_key(os_type, cpu_arch), []) + + def check_biz_version(self, package: models.Packages, bk_biz_id: int): + """如果设定了业务最大版本,则判断当前版本是否大于业务设定的最大版本""" + plugin_version_config = self.plugin_version_config() + if str(bk_biz_id) in plugin_version_config: + biz_version_config = plugin_version_config[str(bk_biz_id)] + biz_version = next( + ( + biz_plugin_version + for biz_plugin_name, biz_plugin_version in biz_version_config.items() + if package.project == biz_plugin_name + ), + None, + ) + if biz_version: + version_str = getattr(package, "version", "") + tag_name__obj_map: Dict[str, Tag] = PluginTargetHelper.get_tag_name__obj_map( + target_id=self.plugin_desc.id, + ) + if version_str in tag_name__obj_map: + version_str = tag_name__obj_map[version_str].target_version + if version.Version(version_str) > version.Version(biz_version): + package = self.get_biz_max_package(package.project, package.os, package.cpu_arch, biz_version) + return package + + @staticmethod + def get_biz_max_package(plugin_name: str, os_type: str, cpu_arch: str, biz_version: str): + """获取业务锁定版本的插件包""" + packages = models.Packages.objects.filter(project=plugin_name, os=os_type, cpu_arch=cpu_arch) + lte_biz_version_packages = [] + for package in packages: + try: + pkg_version = version.Version(package.version) + if pkg_version <= version.Version(biz_version): + lte_biz_version_packages.append(package) + except version.InvalidVersion: + continue + max_version_package = None + if lte_biz_version_packages: + max_version_package = max(lte_biz_version_packages, key=lambda pkg: version.Version(pkg.version)) + return max_version_package + + @staticmethod + def plugin_version_config(): + """业务锁定版本配置""" + plugin_version_config: Dict[str, Dict[str, str]] = models.GlobalSettings.get_config( + models.GlobalSettings.KeyEnum.PLUGIN_VERSION_CONFIG.value, default={} + ) + return plugin_version_config + + @staticmethod + def is_pkg_in_selected_pkg(package: models.Packages, selected_pkg_infos: List[Dict]) -> bool: + for pkg_info in selected_pkg_infos: + if ( + package.project == pkg_info["project"] + and package.id == pkg_info["id"] + and package.version == pkg_info["version"] + and package.os == pkg_info["os"] + and package.cpu_arch == pkg_info["cpu_arch"] + ): + return True + return False diff --git a/apps/backend/tests/plugin/utils.py b/apps/backend/tests/plugin/utils.py index 1a873280e..e9c2addda 100644 --- a/apps/backend/tests/plugin/utils.py +++ b/apps/backend/tests/plugin/utils.py @@ -149,27 +149,10 @@ "type": "object", "required": True, "properties": { - "token": { - "title": "token", - "type": "string", - "required": True - }, - "logVerbosity": { - "title": "logVerbosity", - "type": "number", - "required": False, - "default": 5 - }, - "tempDir": { - "title": "tempDir", - "type": "string", - "required": False - }, - "uid": { - "title": "uid", - "type": "string", - "required": False - }, + "token": {"title": "token", "type": "string", "required": True}, + "logVerbosity": {"title": "logVerbosity", "type": "number", "required": False, "default": 5}, + "tempDir": {"title": "tempDir", "type": "string", "required": False}, + "uid": {"title": "uid", "type": "string", "required": False}, "labels": { "title": "labels", "type": "array", @@ -177,17 +160,8 @@ "title": "label", "type": "object", "required": False, - "properties": { - "key": { - "title": "key", - "type": "string" - }, - "value": { - "title": "value", - "type": "string" - } - } - } + "properties": {"key": {"title": "键", "type": "string"}, "value": {"title": "值", "type": "string"}}, + }, }, "apps": { "title": "apps", @@ -196,16 +170,8 @@ "title": "named_label", "type": "object", "properties": { - "name": { - "title": "name", - "type": "string", - "required": True - }, - "uid": { - "title": "uid", - "type": "string", - "required": False - }, + "name": {"title": "name", "type": "string", "required": True}, + "uid": {"title": "uid", "type": "string", "required": False}, "labels": { "title": "labels", "type": "array", @@ -215,21 +181,15 @@ "type": "object", "required": False, "properties": { - "key": { - "title": "key", - "type": "string" - }, - "value": { - "title": "value", - "type": "string" - } - } - } - } - } - } - } - } + "key": {"title": "键", "type": "string"}, + "value": {"title": "值", "type": "string"}, + }, + }, + }, + }, + }, + }, + }, } # 插件名称 diff --git a/apps/node_man/models.py b/apps/node_man/models.py index 173e1bf8c..1eb2c3d1c 100644 --- a/apps/node_man/models.py +++ b/apps/node_man/models.py @@ -179,6 +179,8 @@ class KeyEnum(Enum): CMDB_INTERNAL_CLOUD_IDS = "CMDB_INTERNAL_CLOUD_IDS" # GSE查询进程状态信息分片大小 QUERY_PROC_STATUS_HOST_LENS = "QUERY_PROC_STATUS_HOST_LENS" + # 业务最大插件版本 + PLUGIN_VERSION_CONFIG = "PLUGIN_VERSION_CONFIG" key = models.CharField(_("键"), max_length=255, db_index=True, primary_key=True) v_json = JSONField(_("值"))