diff --git a/app.yml b/app.yml index 2349e18..bf10054 100644 --- a/app.yml +++ b/app.yml @@ -1,15 +1,16 @@ app_code: bk_gsekit app_name: 进程配置管理 -app_name_en: bk-gsekit +app_name_en: GSEKit is_use_celery: true author: 蓝鲸智云 language: Python -introduction: 进程配置管理 -introduction_en: Process&config manager +introduction: 进程配置管理是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。 +introduction_en: Process&config manager is a SaaS tool launched by Tencent BlueKing + that focuses on process and configuration file management. description: 进程配置管理是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。 description_en: Process&config manager is a SaaS tool launched by Tencent BlueKing that focuses on process and configuration file management. -version: 1.0.31 +version: 1.0.32 category: 运维工具 language_support: 英语,中文 desktop: diff --git a/app_desc.yaml b/app_desc.yaml index 30a0c5b..b0dec6c 100644 --- a/app_desc.yaml +++ b/app_desc.yaml @@ -4,11 +4,11 @@ app: region: default bk_app_code: "bk_gsekit" bk_app_name: 进程配置管理 - bk_app_name_en: bk-gsekit + bk_app_name_en: GSEKit market: category: 运维工具 - introduction: 进程配置管理 - introduction_en: Process&config manager + introduction: 进程配置管理是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。 + introduction_en: Process&config manager is a SaaS tool launched by Tencent BlueKing that focuses on process and configuration file management. description: 进程配置管理是腾讯蓝鲸智云推出的一个专注于进程和配置文件管理的 SaaS 工具。 description_en: Process&config manager is a SaaS tool launched by Tencent BlueKing that focuses on process and configuration file management. display_options: diff --git a/apps/gsekit/configfile/handlers/config_template.py b/apps/gsekit/configfile/handlers/config_template.py index e28aad5..2001d65 100644 --- a/apps/gsekit/configfile/handlers/config_template.py +++ b/apps/gsekit/configfile/handlers/config_template.py @@ -407,17 +407,19 @@ def fill_with_is_bound(cls, config_templates: List[Dict]) -> List[Dict]: config_version_count["config_template_id"]: config_version_count["config_version_count"] for config_version_count in config_version_counts } - - has_release_config_tmpl_ids = set( - ConfigInstance.objects.filter(config_template_id__in=config_template_ids, is_released=True).values_list( - "config_template_id", flat=True - ) - ) + # 返回数量太多出现慢查询 + # has_release_config_tmpl_ids = set( + # ConfigInstance.objects.filter(config_template_id__in=config_template_ids, is_released=True).values_list( + # "config_template_id", flat=True + # ) + # ) for config_template in config_templates: config_template_id = config_template["config_template_id"] relation_count = config_template_binding_count_map[config_template_id] config_template["relation_count"] = relation_count config_template["is_bound"] = bool(sum(relation_count.values())) - config_template["has_release"] = config_template_id in has_release_config_tmpl_ids + config_template["has_release"] = ConfigInstance.objects.filter( + config_template_id=config_template_id, is_released=True + ).exists() config_template["has_version"] = bool(config_template_version_map.get(config_template_id, 0)) return config_templates diff --git a/apps/gsekit/meta/models.py b/apps/gsekit/meta/models.py index 6f7a09a..aa8efef 100644 --- a/apps/gsekit/meta/models.py +++ b/apps/gsekit/meta/models.py @@ -37,6 +37,7 @@ class KEYS: SYNC_BIZ_PROCESS_STATUS_TIMEOUT = "SYNC_BIZ_PROCESS_STATUS_TIMEOUT" # 记录所有业务ID,用于同步新业务到灰度列表对比使用 ALL_BIZ_IDS = "ALL_BIZ_IDS" + SYNC_PROC_STATUS_TIME = "SYNC_PROC_STATUS_TIME" @classmethod def process_task_aggregate_info(cls, bk_biz_id: int) -> typing.Dict[str, str]: diff --git a/apps/gsekit/periodic_tasks/sync_process.py b/apps/gsekit/periodic_tasks/sync_process.py index 598b419..182da15 100644 --- a/apps/gsekit/periodic_tasks/sync_process.py +++ b/apps/gsekit/periodic_tasks/sync_process.py @@ -25,7 +25,10 @@ @task(ignore_result=True) def sync_biz_process_task(bk_biz_id): - ProcessHandler(bk_biz_id=bk_biz_id).sync_biz_process() + logger.info(f"[sync_biz_process_task] start, bk_biz_id={bk_biz_id}") + process_related_infos = ProcessHandler(bk_biz_id=bk_biz_id).sync_biz_process() + ProcessHandler(bk_biz_id=bk_biz_id).sync_biz_process_status(process_related_infos=process_related_infos) + logger.info(f"[sync_biz_process_task] finished, bk_biz_id={bk_biz_id}") @periodic_task(run_every=django_celery_beat.tzcrontab.TzAwareCrontab(minute="*/10", tz=timezone.get_current_timezone())) @@ -38,10 +41,8 @@ def sync_process(bk_biz_id=None): count = len(bk_biz_id_list) for index, biz_id in enumerate(bk_biz_id_list): logger.info(f"[sync_process] start, bk_biz_id={biz_id}") - countdown = calculate_countdown(count, index) + countdown = calculate_countdown(count, index) if count > 1 else 0 sync_biz_process_task.apply_async((biz_id,), countdown=countdown) - # TODO 由于GSE接口存在延迟,此处暂停同步状态的周期任务,待GSE优化后再开启 - # ProcessHandler(bk_biz_id=biz_id).sync_proc_status_to_db() logger.info(f"[sync_process] bk_biz_id={biz_id} will be run after {countdown} seconds.") @@ -54,9 +55,9 @@ def sync_new_biz_to_gray_scope_list(): logger.info(f"sync_new_biz_to_gray_scope_list: {task_id} Start adding new biz to GSE2_GRAY_SCOPE_LIST.") all_biz_ids = GlobalSettings.get_config(key=GlobalSettings.KEYS.ALL_BIZ_IDS, default=[]) - if not all_biz_ids: - logger.info(f"sync_new_biz_to_gray_scope_list: {task_id} No need to add new biz to GSE2_GRAY_SCOPE_LIST.") - return None + # if not all_biz_ids: + # logger.info(f"sync_new_biz_to_gray_scope_list: {task_id} No need to add new biz to GSE2_GRAY_SCOPE_LIST.") + # return None cc_all_biz_ids: List[int] = list(CMDBHandler.biz_id_name_without_permission().keys()) new_biz_ids: List[int] = list(set(cc_all_biz_ids) - set(all_biz_ids)) @@ -65,7 +66,10 @@ def sync_new_biz_to_gray_scope_list(): if new_biz_ids: with transaction.atomic(): # 更新全部业务列表 - GlobalSettings.update_config(key=GlobalSettings.KEYS.ALL_BIZ_IDS, value=cc_all_biz_ids) + if GlobalSettings.objects.filter(key=GlobalSettings.KEYS.ALL_BIZ_IDS): + GlobalSettings.update_config(key=GlobalSettings.KEYS.ALL_BIZ_IDS, value=cc_all_biz_ids) + else: + GlobalSettings.set_config(key=GlobalSettings.KEYS.ALL_BIZ_IDS, value=cc_all_biz_ids) # 对新业务执行灰度操作 result = GrayHandler.build({"bk_biz_ids": new_biz_ids}) diff --git a/apps/gsekit/pipeline_plugins/components/collections/gse.py b/apps/gsekit/pipeline_plugins/components/collections/gse.py index 3c8e442..e780d4f 100644 --- a/apps/gsekit/pipeline_plugins/components/collections/gse.py +++ b/apps/gsekit/pipeline_plugins/components/collections/gse.py @@ -10,7 +10,7 @@ """ import json import logging -from typing import Dict +from typing import Any, Dict, List from django.db.models import F from django.utils.translation import ugettext as _ @@ -27,6 +27,8 @@ from apps.gsekit.process.models import Process, ProcessInst from apps.utils.mako_utils.render import mako_render from dataclasses import dataclass + +from env.constants import GseVersion from .base import CommonData from apps.adapters.api.gse import get_gse_api_helper from apps.adapters.api.gse.base import GseApiBaseHelper @@ -269,10 +271,24 @@ def _execute(self, data, parent_data, common_data): op_type = data.get_one_of_inputs("op_type") data.outputs.proc_op_status_map = {} - proc_operate_req = [] + proc_operate_req: Dict[str, Dict[str, Any]] = {} + no_agent_id_job_task_ids: List[int] = [] for job_task in job_tasks: host_info = job_task.extra_data["process_info"]["host"] + if all([common_data.gse_api_helper.version == GseVersion.V2.value, not host_info.get("bk_agent_id", "")]): + # 对于GseV2来说必须使用agentid进行操作,如果没有agentid可能会导致任务整个handling + no_agent_id_job_task_ids.append(job_task.id) + error_msg = _("该主机无bk_agent_id无法进行相关操作, 请检查主机Agent是否正常") + job_task.set_status( + JobStatus.FAILED, + extra_data={ + "failed_reason": self.generate_proc_op_error_msg(GseDataErrorCode.OP_FAILED, error_msg), + "err_code": GseDataErrorCode.OP_FAILED, + }, + ) + continue + process_info = job_task.extra_data["process_info"]["process"] set_info = job_task.extra_data["process_info"]["set"] module_info = job_task.extra_data["process_info"]["module"] @@ -298,24 +314,24 @@ def _execute(self, data, parent_data, common_data): } self.is_op_cmd_configured(op_type, process_info, raise_exception=True) - proc_operate_req.append( - { + local_inst_name: str = f"{process_info['bk_process_name']}_{local_inst_id}" + + host_identity: Dict[str, Any] = { + "bk_host_innerip": host_info["bk_host_innerip"], + "bk_cloud_id": host_info["bk_cloud_id"], + "bk_agent_id": host_info.get("bk_agent_id", ""), + } + + if local_inst_name in proc_operate_req: + proc_operate_req[local_inst_name]["hosts"].append(host_identity) + else: + proc_operate_req[local_inst_name] = { "meta": { "namespace": NAMESPACE.format(bk_biz_id=process_info["bk_biz_id"]), - "name": f"{process_info['bk_process_name']}_{local_inst_id}", - "labels": { - "bk_process_name": process_info["bk_process_name"], - "bk_process_id": process_info["bk_process_id"], - }, + "name": local_inst_name, }, "op_type": op_type, - "hosts": [ - { - "bk_host_innerip": host_info["bk_host_innerip"], - "bk_cloud_id": host_info["bk_cloud_id"], - "bk_agent_id": host_info.get("bk_agent_id", ""), - } - ], + "hosts": [host_identity], "spec": { "identity": { "index_key": "", @@ -339,24 +355,34 @@ def _execute(self, data, parent_data, common_data): }, }, } - ) + # pipeline-engine会把data转为json,不能用int作为key data.outputs.proc_op_status_map[str(job_task.id)] = GseDataErrorCode.RUNNING - task_id = common_data.gse_api_helper.operate_proc_multi(proc_operate_req=proc_operate_req) + + if not proc_operate_req.values(): + self.finish_schedule() + return True + + task_id = common_data.gse_api_helper.operate_proc_multi(proc_operate_req=list(proc_operate_req.values())) data.outputs.task_id = task_id + data.outputs.no_agent_id_job_task_ids = no_agent_id_job_task_ids return self.return_data(result=True) def _schedule(self, data, parent_data, common_data, callback_data=None): job_tasks = data.get_one_of_inputs("job_tasks") op_type = data.get_one_of_inputs("op_type") task_id = data.get_one_of_outputs("task_id") + no_agent_id_job_task_ids = data.get_one_of_outputs("no_agent_id_job_task_ids", []) + gse_api_result = common_data.gse_api_helper.get_proc_operate_result(task_id) if gse_api_result["code"] == GSE_RUNNING_TASK_CODE: # 查询的任务等待执行中,还未入到redis,继续下一次查询 return self.return_data(result=True) for job_task in job_tasks: + if job_task.id in no_agent_id_job_task_ids: + continue local_inst_id = job_task.extra_data["local_inst_id"] task_result = self.get_job_task_gse_result(gse_api_result, job_task, common_data) error_code = task_result.get("error_code") @@ -366,7 +392,6 @@ def _schedule(self, data, parent_data, common_data, callback_data=None): continue data.outputs.proc_op_status_map[str(job_task.id)] = error_code - if error_code == GseDataErrorCode.SUCCESS: process_inst = ProcessInst.objects.get( bk_process_id=job_task.bk_process_id, local_inst_id=local_inst_id diff --git a/apps/gsekit/process/exceptions.py b/apps/gsekit/process/exceptions.py index 3bef7dd..08f2354 100644 --- a/apps/gsekit/process/exceptions.py +++ b/apps/gsekit/process/exceptions.py @@ -56,3 +56,9 @@ class ProcessNotMatchException(ProcessBaseException): ERROR_CODE = "006" MESSAGE = _("查询进程不匹配") MESSAGE_TPL = _("查询进程不匹配: {user_bk_process_id} vs {cc_bk_process_id}") + + +class ProcessNoAgentIDException(ProcessBaseException): + ERROR_CODE = "007" + MESSAGE = _("找不到带有agent_id的进程进行状态同步,请联系管理员") + MESSAGE_TPL = _("找不到带有agent_id的进程进行状态同步,请联系管理员") diff --git a/apps/gsekit/process/handlers/process.py b/apps/gsekit/process/handlers/process.py index 73e6820..514e6cc 100644 --- a/apps/gsekit/process/handlers/process.py +++ b/apps/gsekit/process/handlers/process.py @@ -9,13 +9,14 @@ See the License for the specific language governing permissions and limitations under the License. """ import copy +from datetime import datetime import json import operator import time from collections import defaultdict from functools import reduce from itertools import groupby -from typing import List, Dict, Union +from typing import Any, List, Dict, Union from django.db import transaction from django.db.models import Q, QuerySet @@ -43,6 +44,7 @@ from common.log import logger from apps.adapters.api.gse import get_gse_api_helper from apps.core.gray.tools import GrayTools +from env.constants import GseVersion class ProcessHandler(APIModel): @@ -512,6 +514,8 @@ def sync_biz_process(self): self.create_process_inst(process_list) + return process_list + def generate_process_inst_migrate_data(self, process_list: List) -> Dict: """计算准备好变更实例所需的数据""" @@ -947,8 +951,8 @@ def sync_proc_status_to_db(self, proc_status_infos=None, gse_api_helper: GseApiB def get_proc_inst_status_infos( proc_inst_infos, _request=None, gse_api_helper: GseApiBaseHelper = None ) -> List[Dict]: - proc_operate_req_slice = [] meta_key_uniq_key_map = {} + proc_operate_req: Dict[str, Dict[str, Any]] = {} for proc_inst_info in proc_inst_infos: host_info = proc_inst_info["host_info"] process_info = proc_inst_info["process_info"] @@ -983,26 +987,34 @@ def get_proc_inst_status_infos( meta_key: str = gse_api_helper.get_gse_proc_key( host_info, namespace=namespace, proc_name=f"{process_info['bk_process_name']}_{local_inst_id}" ) + bk_agent_id: str = host_info.get("bk_agent_id", "") + if gse_api_helper.version == GseVersion.V2.value and not bk_agent_id: + # 对于V2来说必须使用agentid进行查询 + logger.info( + f"get_proc_inst_status failed-> namespace: {namespace}, meta_key:{meta_key}, uniq_key: {uniq_key}" + ) + continue meta_key_uniq_key_map[meta_key] = uniq_key - proc_operate_req_slice.append( - { + + local_inst_name: str = f"{process_info['bk_process_name']}_{local_inst_id}" + + host_identity: Dict[str, Any] = { + "bk_host_innerip": host_info["bk_host_innerip"], + "bk_cloud_id": host_info["bk_cloud_id"], + "bk_agent_id": host_info.get("bk_agent_id", ""), + } + + if local_inst_name in proc_operate_req: + proc_operate_req[local_inst_name]["hosts"].append(host_identity) + else: + proc_operate_req[local_inst_name] = { "meta": { "namespace": namespace, - "name": f"{process_info['bk_process_name']}_{local_inst_id}", - "labels": { - "bk_process_name": process_info["bk_process_name"], - "bk_process_id": process_info["bk_process_id"], - }, + "name": local_inst_name, }, "op_type": GseOpType.CHECK, - "hosts": [ - { - "bk_host_innerip": host_info["bk_host_innerip"], - "bk_cloud_id": host_info["bk_cloud_id"], - "bk_agent_id": host_info.get("bk_agent_id", ""), - } - ], + "hosts": [host_identity], "spec": { "identity": { "index_key": "", @@ -1026,9 +1038,9 @@ def get_proc_inst_status_infos( }, }, } - ) - - gse_task_id: str = gse_api_helper.operate_proc_multi(proc_operate_req=proc_operate_req_slice) + if not proc_operate_req.values(): + raise exceptions.ProcessNoAgentIDException() + gse_task_id: str = gse_api_helper.operate_proc_multi(proc_operate_req=list(proc_operate_req.values())) proc_inst_status_infos = [] uniq_keys_recorded = set() @@ -1109,11 +1121,11 @@ def get_proc_inst_status_infos( ) return proc_inst_status_infos - def sync_biz_process_status(self): + def sync_biz_process_status(self, process_related_infos=None): begin_time = time.time() - - process_related_infos = batch_request(CCApi.list_process_related_info, {"bk_biz_id": self.bk_biz_id}) + if not process_related_infos: + process_related_infos = batch_request(CCApi.list_process_related_info, {"bk_biz_id": self.bk_biz_id}) bk_process_ids = [process_info["process"]["bk_process_id"] for process_info in process_related_infos] proc_inst_map = defaultdict(list) for proc_inst in ProcessInst.objects.filter(bk_process_id__in=bk_process_ids).values( @@ -1164,6 +1176,14 @@ def sync_biz_process_status(self): cost_time = time.time() - begin_time logger.info("[sync_proc_status] cost: {cost_time}s".format(cost_time=cost_time)) + # 记录同步时间 + with transaction.atomic(): + sync_proc_status_time, _ = GlobalSettings.objects.select_for_update().get_or_create( + key=GlobalSettings.KEYS.SYNC_PROC_STATUS_TIME, defaults={"v_json": {}} + ) + sync_proc_status_time.v_json[str(self.bk_biz_id)] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + sync_proc_status_time.save() + return {"cost_time": cost_time} def process_instance_simple( @@ -1207,3 +1227,9 @@ def process_info(self, bk_process_id): # 若有疑问请联系 CMDB 排查 raise ProcessNotMatchException(user_bk_process_id=bk_process_id, cc_bk_process_id=cc_process_id) return process_list[0] + + def sync_process_status_time(self): + sync_process_status_time: Dict[str:str] = GlobalSettings.get_config( + key=GlobalSettings.KEYS.SYNC_PROC_STATUS_TIME, default={} + ) + return {"time": sync_process_status_time.get(str(self.bk_biz_id), "")} diff --git a/apps/gsekit/process/views/process.py b/apps/gsekit/process/views/process.py index 32b9998..5804c12 100644 --- a/apps/gsekit/process/views/process.py +++ b/apps/gsekit/process/views/process.py @@ -240,3 +240,8 @@ def process_instance_simple(self, request, bk_biz_id, *args, **kwargs): expression=self.validated_data.get("expression"), ) ) + + @swagger_auto_schema(operation_summary="获取业务进程同步时间", tags=ProcessViewTags) + @action(detail=False, methods=["GET"]) + def sync_process_status_time(self, request, bk_biz_id, *args, **kwargs): + return Response(ProcessHandler(bk_biz_id=bk_biz_id).sync_process_status_time()) diff --git a/common/context_processors.py b/common/context_processors.py index de1639d..22b4089 100644 --- a/common/context_processors.py +++ b/common/context_processors.py @@ -20,8 +20,8 @@ 除setting外的其他context_processor内容,均采用组件的方式(string) """ WEB_TITLE_MAP = { - "ieod": _("{app_name} | 腾讯蓝鲸智云").format(app_name=settings.APP_NAME), - "open": _("{app_name} | 腾讯蓝鲸智云").format(app_name=settings.APP_NAME), + "ieod": _("{app_name} | 蓝鲸智云").format(app_name=settings.APP_NAME), + "open": _("{app_name} | 蓝鲸智云").format(app_name=settings.APP_NAME), } @@ -58,4 +58,7 @@ def mysetting(request): "CMDB_URL": settings.BK_CC_HOST, "TAM_AEGIS_KEY": settings.TAM_AEGIS_KEY, "TAM_AEGIS_URL": settings.TAM_AEGIS_URL, + "BKPAAS_SHARED_RES_URL": settings.BKPAAS_SHARED_RES_URL, + "BK_COMPONENT_API_URL": settings.BK_COMPONENT_API_OVERWRITE_URL, + "BK_DOMAIN": settings.BK_DOMAIN, } diff --git a/config/__init__.py b/config/__init__.py index d0143de..9ef2af7 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -18,7 +18,6 @@ # This will make sure the app is always imported when # Django starts so that shared_task will use this app. from blueapps.core.celery import celery_app -from django.utils.translation import ugettext_lazy as _ # app 基本信息 @@ -49,4 +48,4 @@ def get_env_or_raise(key): BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # GSEKit 对于外部用户来说理解成本比较高,于是通过 RUN_ENV 区分内外部应用名称 -APP_NAME = (_("进程配置管理"), _("GSEKit"))[RUN_VER == "ieod"] +APP_NAME = "GSEKit" diff --git a/config/default.py b/config/default.py index 734463f..7edaab0 100644 --- a/config/default.py +++ b/config/default.py @@ -209,6 +209,7 @@ BK_CC_HOST = os.environ.get("BK_CC_HOST", BK_PAAS_HOST.replace("paas", "cmdb")) BK_SAAS_HOST = env.BK_SAAS_HOST +BK_DOMAIN = os.environ.get("BKPAAS_BK_DOMAIN", "") BK_ADMIN_USERNAME = os.getenv("BKAPP_ADMIN_USERNAME", "admin") @@ -251,6 +252,9 @@ TAM_AEGIS_KEY = os.getenv("BKAPP_TAM_AEGIS_KEY") TAM_AEGIS_URL = os.getenv("BKAPP_TAM_AEGIS_URL") +# 平台公共信息 +BKPAAS_SHARED_RES_URL = os.getenv("BKPAAS_SHARED_RES_URL", "") + # ============================================================================== # Cache # ============================================================================== diff --git a/requirements.txt b/requirements.txt index 3a2c279..ae5f6e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,14 @@ # sed -i 's|blueapps-open|blueapps|g' requirements.txt # 确保内外部版本统一,打包脚本的替换规则为:blueapps-open -> blueapps,不替换版本号 -blueapps[opentelemetry]==4.7.0 +blueapps[opentelemetry]==4.14.0 python-json-logger==0.1.7 requests==2.22.0 MarkupSafe==1.1.1 django-dbconn-retry==0.1.5 # django -django==3.2.4 +django==3.2.15 django-celery-beat==2.2.0 #django-celery-results==2.0.0 django-filter==2.4.0 diff --git a/web/build/webpack.prod.conf.js b/web/build/webpack.prod.conf.js index 440390e..fe94232 100644 --- a/web/build/webpack.prod.conf.js +++ b/web/build/webpack.prod.conf.js @@ -150,6 +150,11 @@ const prodConf = merge(baseConf, { // webpack4 这个属性暂时设置为 none,参见 https://github.com/jantimon/html-webpack-plugin/issues/870 chunksSortMode: 'none' }), + new HtmlWebpackPlugin({ + filename: 'login_success.html', + template: resolve(__dirname, '../login_success.html'), + inject: false, + }), new MiniCssExtractPlugin({ ignoreOrder: true, diff --git a/web/index-dev.html b/web/index-dev.html index d716e04..4dd2455 100644 --- a/web/index-dev.html +++ b/web/index-dev.html @@ -4,7 +4,7 @@ -
close-line
+down-line
+edit-fill
+exit-full-screen-line
+full-screen-line-line
+help-document-fill
+help-document-line
+incomplete-line
+masterplate-fill
+parenet-node-line
+strategy-fill
+process-manager-fill
+status-fill
+switch-line
+swither-small
+up-line
+jump-fill
+filter-fill
+check-line
+copy
+paste
+updating
+environment
+environment-2
+cc-lock
+lock-radius
+alert
+logout-fill
+shouqi
+zhankai
+weitongbu
+reduce-fill
+shrink-fill
+expand-fill
+angle-left-line
+unfinished
+correct
+bukejian
+kejian
+lang-zh-cn
+lang-en
++ <svg aria-hidden="true"> + <use xlink:href="#icon-xxx"></use> + </svg> ++