From 87a6c1746f376a5e987aaae9238a564da5393d4d Mon Sep 17 00:00:00 2001 From: chalice-1831 <844589474@qq.com> Date: Mon, 4 Nov 2024 15:20:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96excel=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF(closed=20#2437)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/node_man/constants.py | 98 +++++++++++++++ apps/node_man/handlers/excel.py | 191 ++++++++++++++++++++++++++++ apps/node_man/serializers/excel.py | 19 +++ apps/node_man/tools/__init__.py | 1 + apps/node_man/tools/excel.py | 85 +++++++++++++ apps/node_man/tools/host.py | 22 ++++ apps/node_man/urls.py | 192 +++++++++++++++-------------- apps/node_man/views/excel.py | 85 +++++++++++++ requirements.txt | 3 + 9 files changed, 601 insertions(+), 95 deletions(-) create mode 100644 apps/node_man/handlers/excel.py create mode 100644 apps/node_man/serializers/excel.py create mode 100644 apps/node_man/tools/excel.py create mode 100644 apps/node_man/views/excel.py diff --git a/apps/node_man/constants.py b/apps/node_man/constants.py index bee4b0e2b..22c3ab6af 100644 --- a/apps/node_man/constants.py +++ b/apps/node_man/constants.py @@ -1209,3 +1209,101 @@ def _get_member__alias_map(cls) -> Dict[Enum, str]: @classmethod def cpu_type__os_bit_map(cls): return {CpuType.x86: cls.BIT32.value, CpuType.x86_64: cls.BIT64.value, CpuType.aarch64: cls.ARM.value} + + +######################################################################################################## +# EXCEL +######################################################################################################## + + +class ExcelField(EnhanceEnum): + INNER_IPV4 = "inner_ip" + INNER_IPV6 = "inner_ipv6" + OS_TYPE = "os_type" + INSTALL_CHANNEL = "install_channel_id" + LOGIN_PORT = "port" + LOGIN_ACCOUNT = "account" + AUTH_TYPE = "auth_type" + CREDENTIALS = "credentials" + OUTER_IP = "outer_ip" + LOGIN_IP = "login_ip" + BIZ = "bk_biz_id" + CLOUD = "bk_cloud_id" + AP = "ap_id" + TRANSFER_SPEED_LIMIT = "bt_speed_limit" + ADDRESS_TYPE = "bk_addressing" + DATA_COMPRESSION = "enable_compression" + + @classmethod + def _get_member__alias_map(cls) -> Dict[Enum, str]: + return { + cls.INNER_IPV4: _("内网 IPv4"), + cls.INNER_IPV6: _("内网 IPv6"), + cls.OS_TYPE: _("操作系统"), + cls.INSTALL_CHANNEL: _("安装通道"), + cls.LOGIN_PORT: _("登录端口"), + cls.LOGIN_ACCOUNT: _("登录账号"), + cls.AUTH_TYPE: _("认证方式"), + cls.CREDENTIALS: _("凭证"), + cls.OUTER_IP: _("外网 IP"), + cls.LOGIN_IP: _("登录 IP"), + cls.BIZ: _("业务"), + cls.CLOUD: _("管控区域"), + cls.AP: _("接入点"), + cls.TRANSFER_SPEED_LIMIT: _("传输限速"), + cls.ADDRESS_TYPE: _("寻址方式"), + cls.DATA_COMPRESSION: _("数据压缩"), + } + + +EXCEL_REQUIRED = "必填" +EXCEL_OPTIONAL = "可选" +EXCEL_BOTH_NOT_EMPTY = "与「{}」不能同时为空" + +EXCEL_TITLE_OPTIONAL = { + ExcelField.INNER_IPV4.value: EXCEL_BOTH_NOT_EMPTY.format("内网 IPv6"), + ExcelField.INNER_IPV6.value: EXCEL_BOTH_NOT_EMPTY.format("内网 IPv4"), + ExcelField.OS_TYPE.value: EXCEL_REQUIRED, + ExcelField.INSTALL_CHANNEL.value: EXCEL_REQUIRED, + ExcelField.LOGIN_PORT.value: EXCEL_REQUIRED, + ExcelField.LOGIN_ACCOUNT.value: EXCEL_REQUIRED, + ExcelField.AUTH_TYPE.value: EXCEL_REQUIRED, + ExcelField.CREDENTIALS.value: EXCEL_REQUIRED, + ExcelField.OUTER_IP.value: EXCEL_OPTIONAL, + ExcelField.LOGIN_IP.value: EXCEL_OPTIONAL, + ExcelField.BIZ.value: EXCEL_OPTIONAL, + ExcelField.CLOUD.value: EXCEL_OPTIONAL, + ExcelField.AP.value: EXCEL_REQUIRED, + ExcelField.TRANSFER_SPEED_LIMIT.value: EXCEL_OPTIONAL, + ExcelField.ADDRESS_TYPE.value: EXCEL_REQUIRED, + ExcelField.DATA_COMPRESSION.value: EXCEL_OPTIONAL, +} + +EXCEL_TITLE_DESCRIBE = { + ExcelField.INNER_IPV4.value: "目标主机 IPv4 地址。", + ExcelField.INNER_IPV6.value: "目标主机 IPv6 地址。", + ExcelField.OS_TYPE.value: "目标主机操作系统类型。", + ExcelField.INSTALL_CHANNEL.value: "在特殊复杂网络下,目标主机无法与「管控区域」内主机直接连通,可通过指定「安装通道」进行 Agent 安装。默认使用「default」即可。", + ExcelField.LOGIN_PORT.value: "登录到目标主机上的sshd端口。", + ExcelField.LOGIN_ACCOUNT.value: "登录到目标主机上所使用的用户。", + ExcelField.AUTH_TYPE.value: "登录到目标主机上所使用的认证方式。", + ExcelField.CREDENTIALS.value: "登录到目标主机上所使用的凭证,根据认证方式提供密码或私钥,某些「认证方式」的选项可能会忽略这个字段。", + ExcelField.OUTER_IP.value: "会自动注册到 CMDB。", + ExcelField.LOGIN_IP.value: "目标主机的用于登录进行 Agent 安装的 IP 地址,区别于记录在 CMDB 中的 IP;支持 IPv4、IPv6。" + "若未填写,优先使用「内网IPv4」来登录目标机器,若「内网IPv4」未填写,使用「内网IPv6」。", + ExcelField.BIZ.value: "目标主机归属业务。默认使用「蓝鲸」业务", + ExcelField.CLOUD.value: "目标主机所在的管控区域。若是在某个云区域内,选择该云区域的名字。默认使用「直连区域」。", + ExcelField.AP.value: "一般情况下使用「自动选择」即可,若有特殊的接入点无法自动识别到,可以手动选择对应接入点。", + ExcelField.TRANSFER_SPEED_LIMIT.value: "Agent配置中对文件传输速率的硬限制,单位「Mbytes/s」,不填则使用Agent默认值100Mbytes/s。", + ExcelField.ADDRESS_TYPE.value: "记录到 CMDB 中的对应枚举字段。默认为「静态」。", + ExcelField.DATA_COMPRESSION.value: "开启数据压缩后,所有通过数据管道传输的日志采集数据的流量都将进行压缩,可一定程度上降低数据上报所带来的带宽压力。但会带来少量额外的CPU消耗。", +} + + +class ExcelAuthType(EnhanceEnum): + PASSWORD = "PASSWORD" + KEY = "KEY" + + @classmethod + def _get_member__alias_map(cls) -> Dict[Enum, str]: + return {cls.PASSWORD: _("密码"), cls.KEY: _("密钥")} diff --git a/apps/node_man/handlers/excel.py b/apps/node_man/handlers/excel.py new file mode 100644 index 000000000..469b8c409 --- /dev/null +++ b/apps/node_man/handlers/excel.py @@ -0,0 +1,191 @@ +# -*- 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 logging +import re +from typing import Any, Dict, List + +from django.core.files.uploadedfile import InMemoryUploadedFile +from openpyxl import Workbook, load_workbook + +from apps.node_man import constants, models +from apps.node_man.handlers.cmdb import CmdbHandler +from apps.node_man.tools.excel import ExcelTools +from apps.node_man.tools.host import HostTools + +logger = logging.getLogger("app") + +MAIN_SHEET_NAME = "bk_nodeman_info" + +ANALYZE_ERROR_MSG = "第{}行:{}不能为空或格式错误,请依照模板填写,不要修改模板" + + +class ExcelHandler: + @classmethod + def generate_excel_template(cls): + + # 整合数据转为下拉框所需列表, [id]name 格式 + all_install_channel = [ + f"[{item['id']}]{item['name']}" for item in list(models.InstallChannel.objects.all().values()) + ] + all_install_channel.insert(0, "[0]default") + all_biz = [ + f"[{item['bk_biz_id']}]{item['bk_biz_name']}" + for item in CmdbHandler().biz(param={"action": "agent_operate"}) + ] + all_cloud = [ + f"[{item['bk_cloud_id']}]{item['bk_cloud_name']}" for item in list(models.Cloud.objects.all().values()) + ] + all_cloud.insert(0, f"[{constants.DEFAULT_CLOUD}]{constants.DEFAULT_CLOUD_NAME}") + all_ap = [f"[{item['id']}]{item['name']}" for item in list(models.AccessPoint.objects.all().values())] + + all_os = list(constants.OsType) + all_auth_type = [str(type) for type in constants.ExcelAuthType.get_member_value__alias_map().values()] + all_addressing = [str(type) for type in constants.CmdbAddressingType.get_member_value__alias_map().values()] + all_enable_compression = ["True", "False"] + + # 生成excel模板 + excel = Workbook() + excel_sheet = excel.active + excel_sheet.title = MAIN_SHEET_NAME + + excel_field: Dict[Any, str] = constants.ExcelField.get_member_value__alias_map() + excel_field_list = list(excel_field.keys()) + for col, key in enumerate(excel_field_list, start=1): + title_row_cell = excel_sheet.cell(row=1, column=col, value=str(excel_field[key])) + ExcelTools.set_font_style(title_row_cell, font_size=16, color="538DD5", bold=True) + + key_row_cell = excel_sheet.cell(row=2, column=col, value=str(key)) + ExcelTools.set_font_style(key_row_cell, font_size=12, color="538DD5", bold=True) + + optional_row_cell = excel_sheet.cell(row=3, column=col, value=constants.EXCEL_TITLE_OPTIONAL[key]) + if constants.EXCEL_TITLE_OPTIONAL[key] == constants.EXCEL_REQUIRED: + ExcelTools.set_font_style(optional_row_cell, font_size=12, color="C0504D") + else: + ExcelTools.set_font_style(optional_row_cell, font_size=12, color="E26B0A") + + describe_row_cell = excel_sheet.cell(row=4, column=col, value=constants.EXCEL_TITLE_DESCRIBE[key]) + ExcelTools.set_font_style(describe_row_cell, font_size=12, color="000000") + + if key == constants.ExcelField.OS_TYPE.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_os) + elif key == constants.ExcelField.INSTALL_CHANNEL.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_install_channel) + elif key == constants.ExcelField.AUTH_TYPE.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_auth_type) + elif key == constants.ExcelField.BIZ.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_biz) + elif key == constants.ExcelField.CLOUD.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_cloud) + elif key == constants.ExcelField.AP.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_ap) + elif key == constants.ExcelField.ADDRESS_TYPE.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_addressing) + elif key == constants.ExcelField.DATA_COMPRESSION.value: + ExcelTools.create_dropdown(excel, 5, col, key, MAIN_SHEET_NAME, all_enable_compression) + else: + pass + + ExcelTools.fill_color(excel_sheet, 1, 4, 1, len(excel_field_list), "D9D9D9") + ExcelTools.adjust_row_height(excel_sheet, 1, 3, 20) + ExcelTools.adjust_row_height(excel_sheet, 4, 4, 115) + ExcelTools.adjust_col_width(excel_sheet, 1, len(excel_field_list), 35) + ExcelTools.set_alignment(excel_sheet, "center", "left") + + return excel + + def analyze_excel(self, file: InMemoryUploadedFile) -> List[Dict]: + + # 解析excel + excel = load_workbook(filename=file) + excel_sheet = excel.active + keys = [cell.value for cell in excel_sheet[2]] + + # 正则匹配处理 [id]name 类型的下拉框内容 + pattern = r"\[(\d+)\]" + + # 获取加密cipher + cipher = HostTools.get_asymmetric_cipher() + + required_list = [ + key for key, value in constants.EXCEL_TITLE_OPTIONAL.items() if value == constants.EXCEL_REQUIRED + ] + + error_message: List[str] = [] + excel_data = [] + for index, row in enumerate(excel_sheet.iter_rows(min_row=5, values_only=True), start=5): + + row_data = {keys[i]: cell for i, cell in enumerate(row)} + + row_err_msg: List[str] = [] + + if ( + row_data[constants.ExcelField.INNER_IPV4.value] is None + and row_data[constants.ExcelField.INNER_IPV6.value] is None + ): + row_err_msg.append(ANALYZE_ERROR_MSG.format(index, "IP")) + + for key in required_list: + if row_data[key] is None: + row_err_msg.append(ANALYZE_ERROR_MSG.format(index, key)) + + if row_data[constants.ExcelField.INSTALL_CHANNEL.value] is not None: + install_channel = re.findall(pattern, row_data[constants.ExcelField.INSTALL_CHANNEL.value]) + if not install_channel: + row_err_msg.append(ANALYZE_ERROR_MSG.format(index, constants.ExcelField.INSTALL_CHANNEL.value)) + row_data[constants.ExcelField.INSTALL_CHANNEL.value] = int(install_channel[0]) + + if row_data[constants.ExcelField.BIZ.value] is not None: + biz = re.findall(pattern, row_data[constants.ExcelField.BIZ.value]) + if not biz: + row_err_msg.append(ANALYZE_ERROR_MSG.format(index, constants.ExcelField.BIZ.value)) + row_data[constants.ExcelField.BIZ.value] = int(biz[0]) + + if row_data[constants.ExcelField.CLOUD.value] is not None: + cloud = re.findall(pattern, row_data[constants.ExcelField.CLOUD.value]) + if not cloud: + row_err_msg.append(ANALYZE_ERROR_MSG.format(index, constants.ExcelField.CLOUD.value)) + row_data[constants.ExcelField.CLOUD.value] = int(cloud[0]) + + if row_data[constants.ExcelField.AP.value] is not None: + ap = re.findall(pattern, row_data[constants.ExcelField.AP.value]) + if not ap: + row_err_msg.append(ANALYZE_ERROR_MSG.format(index, constants.ExcelField.AP.value)) + row_data[constants.ExcelField.AP.value] = int(ap[0]) + + if len(row_err_msg) > 0: + error_message.extend(row_err_msg) + continue + + credentials: str = str(row_data[constants.ExcelField.CREDENTIALS.value]) + if ( + row_data[constants.ExcelField.AUTH_TYPE.value] + == constants.ExcelAuthType.get_member_value__alias_map()[constants.ExcelAuthType.PASSWORD.value] + ): + row_data[constants.ExcelField.AUTH_TYPE.value] = constants.ExcelAuthType.PASSWORD.value + row_data["password"] = HostTools.encrypt_with_friendly_exc_handle(cipher, credentials, ValueError) + else: + row_data[constants.ExcelField.AUTH_TYPE.value] = constants.ExcelAuthType.KEY.value + row_data["key"] = HostTools.encrypt_with_friendly_exc_handle(cipher, credentials, ValueError) + + del row_data[constants.ExcelField.CREDENTIALS.value] + + if ( + row_data[constants.ExcelField.ADDRESS_TYPE.value] + == constants.CmdbAddressingType.get_member_value__alias_map()[constants.CmdbAddressingType.STATIC.value] + ): + row_data[constants.ExcelField.ADDRESS_TYPE.value] = constants.CmdbAddressingType.STATIC.value + else: + row_data[constants.ExcelField.ADDRESS_TYPE.value] = constants.CmdbAddressingType.DYNAMIC.value + + excel_data.append(row_data) + + res = {"host": excel_data, "error_message": error_message} + return res diff --git a/apps/node_man/serializers/excel.py b/apps/node_man/serializers/excel.py new file mode 100644 index 000000000..2040bf0a6 --- /dev/null +++ b/apps/node_man/serializers/excel.py @@ -0,0 +1,19 @@ +""" +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 rest_framework import serializers + + +class ExcelDownloadSerializer(serializers.Serializer): + pass + + +class ExcelUploadSerializer(serializers.Serializer): + file = serializers.FileField() diff --git a/apps/node_man/tools/__init__.py b/apps/node_man/tools/__init__.py index 1b54df299..dd2b78c95 100644 --- a/apps/node_man/tools/__init__.py +++ b/apps/node_man/tools/__init__.py @@ -14,6 +14,7 @@ 防止handlers互调导致循环依赖 """ +from .excel import ExcelTools # noqa from .host import HostTools # noqa from .host_v2 import HostV2Tools # noqa from .job import JobTools # noqa diff --git a/apps/node_man/tools/excel.py b/apps/node_man/tools/excel.py new file mode 100644 index 000000000..f5d107689 --- /dev/null +++ b/apps/node_man/tools/excel.py @@ -0,0 +1,85 @@ +# -*- 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 typing import List + +from openpyxl import Workbook +from openpyxl.cell.cell import Cell +from openpyxl.styles import Alignment, Font, PatternFill +from openpyxl.styles.fills import FILL_SOLID +from openpyxl.worksheet.datavalidation import DataValidation +from openpyxl.worksheet.worksheet import Worksheet + +DEFAULT_DROP_DOWN_ROW = 1000 + + +class ExcelTools: + @classmethod + def fill_color( + cls, + excel: Worksheet, + start_row: int, + end_row: int, + start_col: int, + end_col: int, + color: str, + fill_type: str = FILL_SOLID, + ): + fill = PatternFill(start_color=color, end_color=color, fill_type=fill_type) + + for row in range(start_row, end_row + 1): + for col in range(start_col, end_col + 1): + excel.cell(row=row, column=col).fill = fill + + @classmethod + def create_dropdown( + cls, excel: Workbook, start_row: int, col: int, src_sheet: str, dst_sheet: str, options: List[str] + ): + sheet = excel.create_sheet(title=src_sheet) + main_sheet = excel[dst_sheet] + for i, option in enumerate(options, start=1): + sheet[f"A{i}"] = option + + dv = DataValidation(type="list", formula1=f"={src_sheet}!$A$1:$A${len(options)}", allow_blank=True) + + # 默认提供1000行数据下拉 + main_sheet.add_data_validation(dv) + dv.add(f"{chr(64 + col)}{start_row}:{chr(64 + col)}{DEFAULT_DROP_DOWN_ROW}") + + @classmethod + def adjust_row_height(cls, excel: Worksheet, start_row: int, end_row: int, height: float): + for row in range(start_row, end_row + 1): + excel.row_dimensions[row].height = height + + @classmethod + def adjust_col_width(cls, excel: Worksheet, start_col: int, end_col: int, width: float): + for col in range(start_col, end_col + 1): + excel.column_dimensions[chr(64 + col)].width = width + + @classmethod + def set_alignment(cls, excel: Worksheet, vertical: str, horizontal: str): + alignment = Alignment(wrap_text=True, vertical=vertical, horizontal=horizontal) + for row in excel.iter_rows(): + for cell in row: + cell.alignment = alignment + + @classmethod + def set_font_style( + cls, + cell: Cell, + font_size: int, + color: str = "000000", + name: str = "SimSun", + bold: bool = False, + italic: bool = False, + strike: bool = False, + ): + font = Font(size=font_size, color=color, name=name, bold=bold, italic=italic, strike=strike) + cell.font = font diff --git a/apps/node_man/tools/host.py b/apps/node_man/tools/host.py index 42b9ab7c9..d5df743cb 100644 --- a/apps/node_man/tools/host.py +++ b/apps/node_man/tools/host.py @@ -95,3 +95,25 @@ def export_all_cloud_area_colon_ip(cloud_id_ip_type: Dict[str, bool], hosts_stat else: result = [] return result + + @classmethod + def encrypt_with_friendly_exc_handle( + cls, cipher: BaseAsymmetricCipher, unencrypt_message: str, raise_exec: Type[Exception] + ) -> str: + """ + 加密友好提示处理 + :param cipher: 密码器 + :param unencrypt_message: + :param raise_exec: + :return: + """ + + try: + encrypt_message: str = cipher.encrypt(unencrypt_message) + except ValueError as e: + raise raise_exec(_("密文无法加密,请检查是否按规则使用密钥加密:{err_msg}").format(err_msg=e)) + except Exception as e: + raise raise_exec(_("密文加密失败:{err_msg").format(err_msg=e)) + + # 加密 + return encrypt_message diff --git a/apps/node_man/urls.py b/apps/node_man/urls.py index faa90fc48..f9fd6b626 100644 --- a/apps/node_man/urls.py +++ b/apps/node_man/urls.py @@ -1,95 +1,97 @@ -# -*- 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 blueapps.account.decorators import login_exempt -from django.conf import settings -from django.conf.urls import url -from django.urls import include -from iam import IAM -from iam.contrib.django.dispatcher import DjangoBasicResourceApiDispatcher -from rest_framework import routers - -from apps.node_man import views -from apps.node_man.iam_provider import ( - ApResourceProvider, - BusinessResourceProvider, - CloudResourceProvider, - PackageResourceProvider, - StrategyResourceProvider, -) -from apps.node_man.views import ( - ap, - cloud, - cmdb, - debug, - host, - install_channel, - job, - meta, - password, - permission, - plugin, - policy, -) -from apps.node_man.views.healthz import HealthzViewSet -from apps.node_man.views.host_v2 import HostV2ViewSet -from apps.node_man.views.plugin import GsePluginViewSet -from apps.node_man.views.plugin_v2 import PluginV2ViewSet -from apps.node_man.views.sync_task import SyncTaskViewSet - -iam = IAM(settings.APP_CODE, settings.SECRET_KEY, settings.BK_IAM_INNER_HOST, settings.BK_COMPONENT_API_OVERWRITE_URL) - -router = routers.DefaultRouter(trailing_slash=True) - -router.register(r"ap", ap.ApViewSet, basename="ap") -router.register(r"cloud", cloud.CloudViewSet, basename="cloud") -router.register(r"install_channel", install_channel.InstallChannelViewSet, basename="install_channel") -router.register(r"host", host.HostViewSet, basename="host") -router.register(r"v2/host", HostV2ViewSet, basename="host_v2") -router.register(r"job", job.JobViewSet, basename="job") -router.register(r"permission", permission.PermissionViewSet, basename="permission") -router.register(r"cmdb", cmdb.CmdbViews, basename="cmdb") -router.register(r"debug", debug.DebugViews, basename="debug") -router.register(r"meta", meta.MetaViews, basename="meta") -router.register(r"tjj", password.PasswordViews, basename="tjj") -router.register(r"policy", policy.PolicyViewSet, basename="policy") -router.register(r"plugin/(?P\w+)/process", GsePluginViewSet) -router.register(r"plugin", plugin.PluginViewSet, basename="plugin") -router.register(r"plugin/(?P[\w-]+)/package", plugin.PackagesViews, basename="package") -router.register(r"plugin/process", plugin.ProcessStatusViewSet, basename="process_status") -router.register(r"v2/plugin", PluginV2ViewSet, basename="plugin_v2") -router.register(r"healthz", HealthzViewSet, basename="healthz") -router.register(r"sync_task", SyncTaskViewSet, basename="sync_task") - -biz_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) -biz_dispatcher.register("biz", BusinessResourceProvider()) -cloud_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) -cloud_dispatcher.register("cloud", CloudResourceProvider()) -ap_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) -ap_dispatcher.register("ap", ApResourceProvider()) -strategy_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) -strategy_dispatcher.register("strategy", StrategyResourceProvider()) -package_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) -package_dispatcher.register("package", PackageResourceProvider()) - -urlpatterns = [ - url(r"^$", views.index), - url(r"^ping/?$", views.ping), - url(r"^version/?$", views.version), - url(r"^metrics/?$", views.metrics), - url(r"^logout/?$", views.user_exit), - url(r"^tools/download/$", views.tools_download), - url(r"api/", include(router.urls)), - url(r"api/iam/v1/biz", biz_dispatcher.as_view([login_exempt])), - url(r"api/iam/v1/cloud", cloud_dispatcher.as_view([login_exempt])), - url(r"api/iam/v1/ap", ap_dispatcher.as_view([login_exempt])), - url(r"api/iam/v1/strategy", strategy_dispatcher.as_view([login_exempt])), - url(r"api/iam/v1/package", package_dispatcher.as_view([login_exempt])), -] +# -*- 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 blueapps.account.decorators import login_exempt +from django.conf import settings +from django.conf.urls import url +from django.urls import include +from iam import IAM +from iam.contrib.django.dispatcher import DjangoBasicResourceApiDispatcher +from rest_framework import routers + +from apps.node_man import views +from apps.node_man.iam_provider import ( + ApResourceProvider, + BusinessResourceProvider, + CloudResourceProvider, + PackageResourceProvider, + StrategyResourceProvider, +) +from apps.node_man.views import ( + ap, + cloud, + cmdb, + debug, + host, + install_channel, + job, + meta, + password, + permission, + plugin, + policy, +) +from apps.node_man.views.excel import ExcelHandlerViewSet +from apps.node_man.views.healthz import HealthzViewSet +from apps.node_man.views.host_v2 import HostV2ViewSet +from apps.node_man.views.plugin import GsePluginViewSet +from apps.node_man.views.plugin_v2 import PluginV2ViewSet +from apps.node_man.views.sync_task import SyncTaskViewSet + +iam = IAM(settings.APP_CODE, settings.SECRET_KEY, settings.BK_IAM_INNER_HOST, settings.BK_COMPONENT_API_OVERWRITE_URL) + +router = routers.DefaultRouter(trailing_slash=True) + +router.register(r"ap", ap.ApViewSet, basename="ap") +router.register(r"cloud", cloud.CloudViewSet, basename="cloud") +router.register(r"install_channel", install_channel.InstallChannelViewSet, basename="install_channel") +router.register(r"host", host.HostViewSet, basename="host") +router.register(r"v2/host", HostV2ViewSet, basename="host_v2") +router.register(r"job", job.JobViewSet, basename="job") +router.register(r"permission", permission.PermissionViewSet, basename="permission") +router.register(r"cmdb", cmdb.CmdbViews, basename="cmdb") +router.register(r"debug", debug.DebugViews, basename="debug") +router.register(r"meta", meta.MetaViews, basename="meta") +router.register(r"tjj", password.PasswordViews, basename="tjj") +router.register(r"policy", policy.PolicyViewSet, basename="policy") +router.register(r"plugin/(?P\w+)/process", GsePluginViewSet) +router.register(r"plugin", plugin.PluginViewSet, basename="plugin") +router.register(r"plugin/(?P[\w-]+)/package", plugin.PackagesViews, basename="package") +router.register(r"plugin/process", plugin.ProcessStatusViewSet, basename="process_status") +router.register(r"v2/plugin", PluginV2ViewSet, basename="plugin_v2") +router.register(r"healthz", HealthzViewSet, basename="healthz") +router.register(r"sync_task", SyncTaskViewSet, basename="sync_task") +router.register(r"excel", ExcelHandlerViewSet, basename="excel") + +biz_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) +biz_dispatcher.register("biz", BusinessResourceProvider()) +cloud_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) +cloud_dispatcher.register("cloud", CloudResourceProvider()) +ap_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) +ap_dispatcher.register("ap", ApResourceProvider()) +strategy_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) +strategy_dispatcher.register("strategy", StrategyResourceProvider()) +package_dispatcher = DjangoBasicResourceApiDispatcher(iam, settings.BK_IAM_SYSTEM_ID) +package_dispatcher.register("package", PackageResourceProvider()) + +urlpatterns = [ + url(r"^$", views.index), + url(r"^ping/?$", views.ping), + url(r"^version/?$", views.version), + url(r"^metrics/?$", views.metrics), + url(r"^logout/?$", views.user_exit), + url(r"^tools/download/$", views.tools_download), + url(r"api/", include(router.urls)), + url(r"api/iam/v1/biz", biz_dispatcher.as_view([login_exempt])), + url(r"api/iam/v1/cloud", cloud_dispatcher.as_view([login_exempt])), + url(r"api/iam/v1/ap", ap_dispatcher.as_view([login_exempt])), + url(r"api/iam/v1/strategy", strategy_dispatcher.as_view([login_exempt])), + url(r"api/iam/v1/package", package_dispatcher.as_view([login_exempt])), +] diff --git a/apps/node_man/views/excel.py b/apps/node_man/views/excel.py new file mode 100644 index 000000000..1b9bea091 --- /dev/null +++ b/apps/node_man/views/excel.py @@ -0,0 +1,85 @@ +# -*- 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 io import BytesIO + +from django.http import StreamingHttpResponse +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response + +from apps.generic import APIViewSet +from apps.node_man.handlers.excel import ExcelHandler +from apps.node_man.serializers.excel import ( + ExcelDownloadSerializer, + ExcelUploadSerializer, +) + +EXCEL_VIEW_TAGS = ["excel"] + + +class ExcelHandlerViewSet(APIViewSet): + @swagger_auto_schema( + operation_summary="获取excel模板", + responses={status.HTTP_200_OK: ExcelDownloadSerializer()}, + tags=EXCEL_VIEW_TAGS, + ) + @action(detail=False, methods=["GET"], serializer_class=ExcelDownloadSerializer) + def download(self, request): + """ + @api {GET} /excel/download/ 获取excel模板 + @apiName download_excel + @apiGroup Excel + @apiParamExample {Json} 请求例子: + { + } + @apiSuccessExample {Json} 成功返回: + { + } + """ + + file = ExcelHandler().generate_excel_template() + output = BytesIO() + file.save(output) + output.seek(0) + + filename = "bk_nodeman_info.xlsx" + response = StreamingHttpResponse(streaming_content=output) + response.headers["Content-Type"] = "application/octet-stream" + response.headers["Content-Disposition"] = 'attachment;filename="{}"'.format(filename) + return response + + @swagger_auto_schema( + operation_summary="上传excel", + request_body=ExcelUploadSerializer(), + tags=EXCEL_VIEW_TAGS, + ) + @action(detail=False, methods=["POST"], serializer_class=ExcelUploadSerializer) + def upload(self, request): + """ + @api {POST} /excel/upload/ 上传excel + @apiName upload_excel + @apiGroup Excel + @apiParam {File} file excel文件 + @apiParamExample {Form} 请求例子: + { + "file": file + } + @apiSuccessExample {Json} 成功返回: + { + } + """ + ser = self.serializer_class(data=request.data) + ser.is_valid(raise_exception=True) + data = ser.validated_data + file = data["file"] + + return Response(ExcelHandler().analyze_excel(file)) diff --git a/requirements.txt b/requirements.txt index 7b019f343..341ab6615 100644 --- a/requirements.txt +++ b/requirements.txt @@ -111,3 +111,6 @@ cryptography==3.3.2 # notice bk-notice-sdk==1.2.0 + +# excel +openpyxl==3.1.3