diff --git a/app.yml b/app.yml index 526153d32c..650371dfe3 100644 --- a/app.yml +++ b/app.yml @@ -6,7 +6,7 @@ is_use_celery: True author: 蓝鲸智云 introduction: 标准运维是通过一套成熟稳定的任务调度引擎,把在多系统间的工作整合到一个流程,助力运维实现跨系统调度自动化的SaaS应用。 introduction_en: SOPS is a SaaS application that utilizes a set of mature and stable task scheduling engines to help realize cross-system scheduling automation, and integrates the work among multiple systems into a single process. -version: 3.29.0 +version: 3.30.0 category: 运维工具 language_support: 中文 desktop: diff --git a/app_desc.yaml b/app_desc.yaml index 3cfd0e1a66..0324f4d24f 100644 --- a/app_desc.yaml +++ b/app_desc.yaml @@ -1,5 +1,5 @@ spec_version: 2 -app_version: "3.29.0" +app_version: "3.30.0" app: region: default bk_app_code: bk_sops diff --git a/config/default.py b/config/default.py index de57d4b699..a52b47afcb 100644 --- a/config/default.py +++ b/config/default.py @@ -16,6 +16,8 @@ from urllib.parse import urlparse from bamboo_engine.config import Settings as BambooSettings +from bkcrypto import constants as bkcrypto_constants +from bkcrypto.asymmetric.options import RSAAsymmetricOptions from blueapps.conf.default_settings import * # noqa from blueapps.conf.log import get_logging_config_dict from blueapps.opentelemetry.utils import inject_logging_trace_info @@ -155,7 +157,9 @@ ENABLE_IPV6 = env.ENABLE_IPV6 # paasv3 和 开启了ipv6 才会尝试加载 BK_API_URL_TMPL 这个变量 -if env.IS_PAAS_V3 or ENABLE_IPV6: +ENABLE_GSE_V2 = env.ENABLE_GSE_V2 + +if env.IS_PAAS_V3 or ENABLE_IPV6 or ENABLE_GSE_V2: BK_API_URL_TMPL = env.BK_APIGW_URL_TMPL if env.IS_PAAS_V3: @@ -207,7 +211,7 @@ # mako模板中: # 如果静态资源修改了以后,上线前改这个版本号即可 -STATIC_VERSION = "3.29.0" +STATIC_VERSION = "3.30.0" DEPLOY_DATETIME = datetime.datetime.now().strftime("%Y%m%d%H%M%S") STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] @@ -672,7 +676,6 @@ def monitor_report_config(): from bk_monitor_report import MonitorReporter # noqa from bk_monitor_report.contrib.celery import MonitorReportStep # noqa - from blueapps.core.celery import celery_app # noqa reporter = MonitorReporter( @@ -787,5 +790,44 @@ def check_engine_admin_permission(request, *args, **kwargs): PIPELINE_ENGINE_ADMIN_API_PERMISSION = "config.default.check_engine_admin_permission" + +BKCRYPTO = { + "ASYMMETRIC_CIPHERS": { + "default": { + "get_key_config": "gcloud.utils.crypto.get_default_asymmetric_key_config", + "cipher_options": { + bkcrypto_constants.AsymmetricCipherType.RSA.value: RSAAsymmetricOptions( + padding=bkcrypto_constants.RSACipherPadding.PKCS1_v1_5 + ) + }, + }, + }, + "SYMMETRIC_CIPHERS": {"default": {"get_key_config": "gcloud.utils.crypto.get_default_symmetric_key_config"}}, +} + +# 启用框架内置数据加密 +BLUEAPPS_ENABLE_DB_ENCRYPTION = True +# 复用已有的 default 对称加密实例 +BKCRYPTO["SYMMETRIC_CIPHERS"]["blueapps"] = BKCRYPTO["SYMMETRIC_CIPHERS"]["default"] + + +# 加密 +if env.BKPAAS_BK_CRYPTO_TYPE == "SHANGMI": + BKCRYPTO_ASYMMETRIC_CIPHER_TYPE = bkcrypto_constants.AsymmetricCipherType.SM2.value + BKCRYPTO.update( + { + "ASYMMETRIC_CIPHER_TYPE": BKCRYPTO_ASYMMETRIC_CIPHER_TYPE, + "SYMMETRIC_CIPHER_TYPE": bkcrypto_constants.SymmetricCipherType.SM4.value, + } + ) +else: + BKCRYPTO_ASYMMETRIC_CIPHER_TYPE = bkcrypto_constants.AsymmetricCipherType.RSA.value + BKCRYPTO.update( + { + "ASYMMETRIC_CIPHER_TYPE": BKCRYPTO_ASYMMETRIC_CIPHER_TYPE, + "SYMMETRIC_CIPHER_TYPE": bkcrypto_constants.SymmetricCipherType.AES.value, + } + ) + # 任务列表过滤失败任务最大天数 TASK_LIST_STATUS_FILTER_DAYS = env.BKPAAS_TASK_LIST_STATUS_FILTER_DAYS diff --git a/config/sites/community/ver_settings.py b/config/sites/community/ver_settings.py index f799638d9a..307c7af1ba 100644 --- a/config/sites/community/ver_settings.py +++ b/config/sites/community/ver_settings.py @@ -37,7 +37,6 @@ LOah9mmRwLJdcfa3Js+jw2lOCmxzqauYZHVHg/hH7g== -----END RSA PRIVATE KEY----- """ -RSA_PRIV_KEY = base64.b64decode(env.RSA_PRIV_KEY).decode("utf-8") if env.RSA_PRIV_KEY else DEFAULT_RSA_PRIV_KEY # PUB_KEY for frontend, which can not use three quotes DEFAULT_RSA_PUB_KEY = ( @@ -49,8 +48,30 @@ + "-----END PUBLIC KEY-----" ) +RSA_PRIV_KEY = base64.b64decode(env.RSA_PRIV_KEY).decode("utf-8") if env.RSA_PRIV_KEY else DEFAULT_RSA_PRIV_KEY RSA_PUB_KEY = base64.b64decode(env.RSA_PUB_KEY).decode("utf-8") if env.RSA_PUB_KEY else DEFAULT_RSA_PUB_KEY + +DEFAULT_SM2_PRIV_KEY = """ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICI+zMQDiQ5/xXmnGxGqLSD++Cp+I601cIFLKRd2yrGBoAoGCCqBHM9V +AYItoUQDQgAE95+i3TAfODAzb9QhJmyUmxH/HocisveqkrafHJ25NO/uCtkb2yXH +vrZcCDmoxeO+z5vp88jN/ulVsl9qEqm6vQ== +-----END EC PRIVATE KEY----- +""" + + +DEFAULT_SM2_PUB_KEY = ( + "-----BEGIN PUBLIC KEY-----\\n" + + "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE95+i3TAfODAzb9QhJmyUmxH/Hoci\\n" + + "sveqkrafHJ25NO/uCtkb2yXHvrZcCDmoxeO+z5vp88jN/ulVsl9qEqm6vQ==\\n" + + "-----END PUBLIC KEY-----" +) + + +SM2_PRIV_KEY = base64.b64decode(env.RSA_PRIV_KEY).decode("utf-8") if env.RSA_PRIV_KEY else DEFAULT_RSA_PRIV_KEY +SM2_PUB_KEY = base64.b64decode(env.RSA_PUB_KEY).decode("utf-8") if env.RSA_PUB_KEY else DEFAULT_SM2_PUB_KEY + # APIGW Auth APIGW_APP_CODE_KEY = "bk_app_code" APIGW_USER_USERNAME_KEY = "bk_username" diff --git a/config/sites/enterprise/ver_settings.py b/config/sites/enterprise/ver_settings.py index 4270ab010c..d109cf0de2 100644 --- a/config/sites/enterprise/ver_settings.py +++ b/config/sites/enterprise/ver_settings.py @@ -37,7 +37,6 @@ T8ow3nMSbvx5X28wOjbk04tmfM/kVqcVhFWhDHjHZzlt -----END RSA PRIVATE KEY----- """ -RSA_PRIV_KEY = base64.b64decode(env.RSA_PRIV_KEY).decode("utf-8") if env.RSA_PRIV_KEY else DEFAULT_RSA_PRIV_KEY # PUB_KEY for frontend, which can not use three quotes DEFAULT_RSA_PUB_KEY = ( @@ -48,7 +47,31 @@ + "iymoAVK67gfTOTvckQIDAQAB\\n" + "-----END PUBLIC KEY-----" ) + RSA_PUB_KEY = base64.b64decode(env.RSA_PUB_KEY).decode("utf-8") if env.RSA_PUB_KEY else DEFAULT_RSA_PUB_KEY +RSA_PRIV_KEY = base64.b64decode(env.RSA_PRIV_KEY).decode("utf-8") if env.RSA_PRIV_KEY else DEFAULT_RSA_PRIV_KEY + + +DEFAULT_SM2_PRIV_KEY = """ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIn5SYKHr3+m/XyC/ECzDJYuwUoTQHDUkIueKFXTjhSBoAoGCCqBHM9V +AYItoUQDQgAEYxBE08d8yEEK2+DZ7F5RsNrUvCZ578lkYsXFDC1fW2IcRecNz8LG +ZWSZGFfgYMeK1f3fIuYBAJVuna/V3FP4tA== +-----END EC PRIVATE KEY----- +""" + + +DEFAULT_SM2_PUB_KEY = ( + "-----BEGIN PUBLIC KEY-----\\n" + + "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEYxBE08d8yEEK2+DZ7F5RsNrUvCZ5\\n" + + "78lkYsXFDC1fW2IcRecNz8LGZWSZGFfgYMeK1f3fIuYBAJVuna/V3FP4tA==\\n" + + "-----END PUBLIC KEY-----" +) + + +SM2_PRIV_KEY = base64.b64decode(env.SM2_PRIV_KEY).decode("utf-8") if env.SM2_PRIV_KEY else DEFAULT_SM2_PRIV_KEY +SM2_PUB_KEY = base64.b64decode(env.SM2_PUB_KEY).decode("utf-8") if env.SM2_PUB_KEY else DEFAULT_SM2_PUB_KEY + # APIGW Auth APIGW_APP_CODE_KEY = "bk_app_code" diff --git a/docs/develop/tag_usage_dev.md b/docs/develop/tag_usage_dev.md index 42e6bca4a0..fd4aec2b71 100644 --- a/docs/develop/tag_usage_dev.md +++ b/docs/develop/tag_usage_dev.md @@ -313,6 +313,8 @@ ip 选择器,支持静态 ip 或动态 ip 的单选和多选。 - `pubKey`: 加密公钥 - `disabled`:设置是否禁用组件 + - `canUseVar`: 是否可以使用全局变量,默认为true + - `textareaMode`: 手动输入密码时,表单类型为textarea,默认为false - `value`:加密后的密码值 **方法** diff --git a/en_docs/develop/tag_usage_dev.md b/en_docs/develop/tag_usage_dev.md index 556f81a44b..4086d96641 100644 --- a/en_docs/develop/tag_usage_dev.md +++ b/en_docs/develop/tag_usage_dev.md @@ -313,7 +313,10 @@ Password input box. **Attributes** + - `pubKey`: crypto public key - `disabled`: set whether this component is disabled. + - `canUseVar`: whether global variables can be used, which defaults to true + - `textareaMode`: When entering a password manually, the form type is textarea and the default is false - `value`: the encrypted password value **Methods** diff --git a/env.py b/env.py index 59a977e857..85c1af5bf0 100644 --- a/env.py +++ b/env.py @@ -98,6 +98,7 @@ ENABLE_SWAGGER_UI = os.getenv("BKAPP_ENABLE_SWAGGER_UI", False) ENABLE_IPV6 = False if os.getenv("BKAPP_ENABLE_IPV6") is None else True +ENABLE_GSE_V2 = int(os.getenv("BKAPP_ENABLE_GSE_V2", 0)) == 1 # 流程最高嵌套层数 TEMPLATE_MAX_RECURSIVE_NUMBER = int(os.getenv("BKAPP_TEMPLATE_MAX_RECURSIVE_NUMBER", 200)) @@ -108,5 +109,14 @@ # 获取 PaaS 注入的蓝鲸域名 BKPAAS_BK_DOMAIN = os.getenv("BKPAAS_BK_DOMAIN", "") or os.getenv("BK_DOMAIN", "") + +# 获取加密类型 +BKPAAS_BK_CRYPTO_TYPE = ( + os.getenv("BKPAAS_BK_CRYPTO_TYPE", "") + or os.getenv("BKAPP_BK_CRYPTO_TYPE", "") + or os.getenv("BK_CRYPTO_TYPE") + or "CLASSIC" +) + # 默认六个月 BKPAAS_TASK_LIST_STATUS_FILTER_DAYS = int(os.getenv("BKPAAS_TASK_LIST_STATUS_FILTER_DAYS", 180)) diff --git a/env_v2.py b/env_v2.py index 1e1c3d88d2..5d772a65cd 100644 --- a/env_v2.py +++ b/env_v2.py @@ -136,6 +136,8 @@ # RSA KEYS, 保存的是密钥的base64加密形式, 使用base64.b64encode(KEY.encode("utf-8"))进行处理后保存为环境变量 RSA_PRIV_KEY = os.getenv("BKAPP_RSA_PRIV_KEY", None) RSA_PUB_KEY = os.getenv("BKAPP_RSA_PUB_KEY", None) +SM2_PRIV_KEY = os.getenv("BKAPP_SM2_PRIV_KEY", None) +SM2_PUB_KEY = os.getenv("BKAPP_SM2_PUB_KEY", None) # 单业务下最大周期任务数量 PERIODIC_TASK_PROJECT_MAX_NUMBER = int(os.getenv("BKAPP_PERIODIC_TASK_PROJECT_MAX_NUMBER", 50)) diff --git a/env_v3.py b/env_v3.py index a3132e3f4a..44a0965753 100644 --- a/env_v3.py +++ b/env_v3.py @@ -167,6 +167,8 @@ # RSA KEYS, 保存的是密钥的base64加密形式, 使用base64.b64encode(KEY.encode("utf-8"))进行处理后保存为环境变量 RSA_PRIV_KEY = os.getenv("BKAPP_RSA_PRIV_KEY", None) RSA_PUB_KEY = os.getenv("BKAPP_RSA_PUB_KEY", None) +SM2_PRIV_KEY = os.getenv("BKAPP_SM2_PRIV_KEY", None) +SM2_PUB_KEY = os.getenv("BKAPP_SM2_PUB_KEY", None) # 单业务下最大周期任务数量 PERIODIC_TASK_PROJECT_MAX_NUMBER = int(os.getenv("BKAPP_PERIODIC_TASK_PROJECT_MAX_NUMBER", 50)) diff --git a/frontend/desktop/package.json b/frontend/desktop/package.json index c89d1b8536..7059bbabf8 100644 --- a/frontend/desktop/package.json +++ b/frontend/desktop/package.json @@ -12,8 +12,9 @@ "license": "ISC", "dependencies": { "@blueking/bkcharts": "^2.0.11-alpha.5", + "@blueking/bkui-form": "0.0.35", + "@blueking/crypto-js-sdk": "0.0.5", "@blueking/user-selector": "^1.0.5-beta.2", - "@blueking/bkui-form": "0.0.11", "@vue/babel-preset-jsx": "^1.3.0", "ajv": "^6.10.2", "art-template": "^4.13.0", diff --git a/frontend/desktop/src/assets/html/index-dev.html b/frontend/desktop/src/assets/html/index-dev.html index 3723ea0d09..27058461c2 100644 --- a/frontend/desktop/src/assets/html/index-dev.html +++ b/frontend/desktop/src/assets/html/index-dev.html @@ -32,6 +32,9 @@ // 是否开启导入 V1 模板的入口 var IMPORT_V1_FLAG = Number('0'); var RSA_PUB_KEY = ''; + var ASYMMETRIC_CIPHER_TYPE = ''; + var ASYMMETRIC_PUBLIC_KEY = ''; + var ASYMMETRIC_PREFIX = ''; var APP_CODE = 'bk_sops'; var FILE_UPLOAD_ENTRY = '/package/upload/'; var MAX_NODE_EXECUTE_TIMEOUT = 6000; diff --git a/frontend/desktop/src/assets/html/index.html b/frontend/desktop/src/assets/html/index.html index f3e246069e..5de8ded620 100644 --- a/frontend/desktop/src/assets/html/index.html +++ b/frontend/desktop/src/assets/html/index.html @@ -42,6 +42,9 @@ // 是否开启导入 V1 模板的入口 var IMPORT_V1_FLAG ={{import_v1_flag}}; var RSA_PUB_KEY = '{{RSA_PUB_KEY}}'; + var ASYMMETRIC_CIPHER_TYPE = '{{ASYMMETRIC_CIPHER_TYPE}}'; + var ASYMMETRIC_PUBLIC_KEY = '{{ASYMMETRIC_PUBLIC_KEY}}'; + var ASYMMETRIC_PREFIX = '{{ASYMMETRIC_PREFIX}}'; var APP_CODE = '{{APP_CODE}}'; var FILE_UPLOAD_ENTRY = '{{FILE_UPLOAD_ENTRY}}'; var MEMBER_SELECTOR_DATA_HOST = '{{MEMBER_SELECTOR_DATA_HOST}}'; diff --git a/frontend/desktop/src/components/common/RenderForm/FormItem.vue b/frontend/desktop/src/components/common/RenderForm/FormItem.vue index 9cef50a2b5..6fdf0213b0 100644 --- a/frontend/desktop/src/components/common/RenderForm/FormItem.vue +++ b/frontend/desktop/src/components/common/RenderForm/FormItem.vue @@ -396,11 +396,15 @@ defaultValueFormat = getDefaultValueFormat(this.scheme) } - const isTypeValid = Array.isArray(defaultValueFormat.type) - ? defaultValueFormat.type.indexOf(valueType) > -1 - : defaultValueFormat.type === valueType + const defaultValueType = Array.isArray(defaultValueFormat.type) ? defaultValueFormat.type : [defaultValueFormat.type] - if (isTypeValid) { + // 处理非密码框表单使用密码变量时,需要展示******的场景 + // 非密码框且值类型包含string的表单,如果当前value为Object类型,且type值为password_value时,展示值为****** + if (this.scheme.type !== 'password' && defaultValueType.includes('String') && checkDataType(val) === 'Object' && val.type === 'password_value') { + return '******' + } + + if (defaultValueType.includes(valueType)) { formValue = tools.deepClone(val) } else { formValue = tools.deepClone(defaultValueFormat.value) @@ -409,6 +413,127 @@ return formValue }, + getDefaultValueFormat () { + let valueFormat + switch (this.scheme.type) { + case 'input': + case 'textarea': + case 'radio': + case 'text': + case 'datetime': + case 'memberSelector': + case 'logDisplay': + case 'code_editor': + case 'section': + valueFormat = { + type: ['String', 'Number', 'Boolean'], + value: '' + } + break + case 'checkbox': + case 'datatable': + case 'tree': + case 'upload': + case 'cascader': + valueFormat = { + type: 'Array', + value: [] + } + break + case 'select': + if (this.scheme.attrs.multiple) { + valueFormat = { + type: 'Array', + value: [] + } + } else { + valueFormat = { + type: ['String', 'Number', 'Boolean'], + value: '' + } + } + break + case 'time': + if (this.scheme.attrs.isRange) { + valueFormat = { + type: 'Array', + value: ['00:00:00', '23:59:59'] + } + } else { + valueFormat = { + type: 'String', + value: '' + } + } + break + case 'int': + valueFormat = { + type: 'Number', + value: 0 + } + break + case 'ip_selector': + valueFormat = { + type: 'Object', + value: { + static_ip_table_config: [], + selectors: [], + ip: [], + topo: [], + group: [], + filters: [], + excludes: [] + } + } + break + case 'set_allocation': + valueFormat = { + type: 'Object', + value: { + config: { + set_count: 1, + set_template_id: '', + host_resources: [], + module_detail: [] + }, + data: [], + separator: ',' + } + } + break + case 'host_allocation': + valueFormat = { + type: 'Object', + value: { + config: { + host_count: 0, + host_screen_value: '', + host_resources: [], + host_filter_detail: [] + }, + data: [], + separator: ',' + } + } + break + case 'password': + valueFormat = { + type: ['String', 'Object'], + value: { + type: 'password_value', + tag: 'value', + value: '' + } + } + break + default: + valueFormat = { + type: 'String', + value: '' + } + } + return valueFormat + }, isRequired () { let required = false if (this.option.showRequired === true && 'validation' in this.scheme.attrs) { diff --git a/frontend/desktop/src/components/common/RenderForm/RenderForm.vue b/frontend/desktop/src/components/common/RenderForm/RenderForm.vue index 0a6a6467c6..e988a08b98 100644 --- a/frontend/desktop/src/components/common/RenderForm/RenderForm.vue +++ b/frontend/desktop/src/components/common/RenderForm/RenderForm.vue @@ -233,7 +233,6 @@ case 'radio': case 'text': case 'datetime': - case 'password': case 'member_selector': case 'section': case 'code_editor': @@ -291,6 +290,13 @@ separator: ',' } break + case 'password': + val = { + type: 'password_value', + tag: 'value', + value: '' + } + break default: val = '' } diff --git a/frontend/desktop/src/components/common/RenderForm/tags/TagPassword.vue b/frontend/desktop/src/components/common/RenderForm/tags/TagPassword.vue index 23a4efe4ff..936b10307d 100644 --- a/frontend/desktop/src/components/common/RenderForm/tags/TagPassword.vue +++ b/frontend/desktop/src/components/common/RenderForm/tags/TagPassword.vue @@ -11,25 +11,56 @@ */ - + diff --git a/frontend/desktop/src/config/i18n/cn.js b/frontend/desktop/src/config/i18n/cn.js index 4b6e116144..e2b63f0f06 100644 --- a/frontend/desktop/src/config/i18n/cn.js +++ b/frontend/desktop/src/config/i18n/cn.js @@ -1766,7 +1766,9 @@ const cn = { '跳过节点将忽略失败继续往后执行': '跳过节点将忽略失败继续往后执行', '确定重试子流程【n】 ?': '确定重试子流程【{n}】 ?', '确定重试节点【n】 ?': '确定重试节点【{n}】 ?', - '非根节点仅支持以原参数进行重试': '非根节点仅支持以原参数进行重试' + '非根节点仅支持以原参数进行重试': '非根节点仅支持以原参数进行重试', + 'password_手动输入': '手动输入', + 'password_使用密码变量': '使用密码变量' } export default cn diff --git a/frontend/desktop/src/config/i18n/en.js b/frontend/desktop/src/config/i18n/en.js index 5c907c1b7e..71bc404eec 100644 --- a/frontend/desktop/src/config/i18n/en.js +++ b/frontend/desktop/src/config/i18n/en.js @@ -1772,8 +1772,8 @@ const en = { '导入选择节点': 'Import the Selection Node', '管理执行方案': 'Manage Node-Group', '使用临时方案': 'Use Temporary Node-Group', - '前往新增方案': 'Go to add plan', - '暂无方案': 'No plan', + '前往新增方案': 'Go To Add Node-Group', + '暂无方案': 'No Node-Group', '方案修改成功': 'Node Group Modified', '默认方案': 'Default', '添加默认方案成功': 'Default Node-Group Added', @@ -1800,7 +1800,9 @@ const en = { '跳过节点将忽略失败继续往后执行': 'Skipping node will ignore the failure and continue executing the next step.', '确定重试子流程【n】 ?': 'Retry this subflow [ {n} ] ?', '确定重试节点【n】 ?': 'Retry this node [ {n} ] ?', - '非根节点仅支持以原参数进行重试': 'Non-root nodes only support retrying with the original parameters.' + '非根节点仅支持以原参数进行重试': 'Non-root nodes only support retrying with the original parameters.', + 'password_手动输入': 'custom input', + 'password_使用密码变量': 'use password var.' } export default en diff --git a/frontend/desktop/src/pages/task/TaskCreate/TaskScheme.vue b/frontend/desktop/src/pages/task/TaskCreate/TaskScheme.vue index 7c2aef585c..d1dc2b359c 100644 --- a/frontend/desktop/src/pages/task/TaskCreate/TaskScheme.vue +++ b/frontend/desktop/src/pages/task/TaskCreate/TaskScheme.vue @@ -9,7 +9,7 @@ {{$t('执行方案')}} -
+
diff --git a/frontend/desktop/src/pages/task/TaskExecute/TaskOperation.vue b/frontend/desktop/src/pages/task/TaskExecute/TaskOperation.vue index 576a4bb4d3..ffeb8564ec 100644 --- a/frontend/desktop/src/pages/task/TaskExecute/TaskOperation.vue +++ b/frontend/desktop/src/pages/task/TaskExecute/TaskOperation.vue @@ -1701,7 +1701,7 @@ ordered.push(treeItem) newOrdered = null } - + if (conditions.length) { conditions.forEach(item => { item.style = `margin-left: ${item.parentId ? 16 : marginLeft + 33}px` @@ -1754,7 +1754,7 @@ }) return result }, - + getNodeSourceMaps (pipelineData) { const sourceMap = pipelineData.line.reduce((acc, cur) => { const { source, target } = cur diff --git a/frontend/desktop/src/pages/template/TemplateEdit/TemplateHeader.vue b/frontend/desktop/src/pages/template/TemplateEdit/TemplateHeader.vue index 4d4d3759c5..62f2d0acee 100644 --- a/frontend/desktop/src/pages/template/TemplateEdit/TemplateHeader.vue +++ b/frontend/desktop/src/pages/template/TemplateEdit/TemplateHeader.vue @@ -83,7 +83,7 @@
- {{ '关闭预览' }} + {{ $t('关闭预览') }}
None: + self.plaintext = "123" * random.randint(1, 100) + self.legacy_cipher = get_asymmetric_cipher( + cipher_type=bkcrypto_constants.AsymmetricCipherType.RSA.value, + cipher_options={ + bkcrypto_constants.AsymmetricCipherType.RSA.value: RSAAsymmetricOptions( + public_key_string=crypto.get_default_asymmetric_key_config( + cipher_type=bkcrypto_constants.AsymmetricCipherType.RSA.value + ).public_key_string, + padding=bkcrypto_constants.RSACipherPadding.PKCS1_v1_5, + ) + }, + ) + + def test_ciphertext_without_prefix(self): + """测试不带前缀的密文,模拟存量数据场景""" + self.assertEqual(crypto.decrypt(ciphertext=self.legacy_cipher.encrypt(self.plaintext)), self.plaintext) + + def test_ciphertext_with_secondary_encryption(self): + """测试原密文再次保存被二次加密的场景""" + ciphertext_without_prefix: str = self.legacy_cipher.encrypt(self.plaintext) + ciphertext: str = AsymmetricCipherSelector().encrypt(ciphertext_without_prefix) + self.assertEqual(crypto.decrypt(ciphertext=ciphertext), self.plaintext) + + def test_ciphertext(self): + """模拟新数据加密携带前缀的场景""" + ciphertext: str = AsymmetricCipherSelector().encrypt(plaintext=self.plaintext) + print(ciphertext) + self.assertTrue(ciphertext.startswith(f"{bkcrypto_constants.AsymmetricCipherType.RSA.value.lower()}_str:::")) + self.assertEqual(crypto.decrypt(ciphertext=ciphertext), self.plaintext) + + def test_plaintext(self): + self.assertEqual(self.plaintext, crypto.decrypt(self.plaintext)) diff --git a/gcloud/utils/crypto.py b/gcloud/utils/crypto.py index b50266cbde..117a0bb1c6 100644 --- a/gcloud/utils/crypto.py +++ b/gcloud/utils/crypto.py @@ -10,69 +10,72 @@ 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 json +import typing import base64 -from Crypto.Cipher import PKCS1_v1_5 as PKCS1_v1_5_cipher -from Crypto.PublicKey import RSA -from Crypto import Util +from bkcrypto import constants as crypto_constants +from bkcrypto.asymmetric.configs import KeyConfig as AsymmetricKeyConfig +from bkcrypto.constants import AsymmetricCipherType +from bkcrypto.contrib.django.ciphers import asymmetric_cipher_manager +from bkcrypto.contrib.django.selectors import AsymmetricCipherSelector +from bkcrypto.symmetric.configs import KeyConfig as SymmetricKeyConfig +from django.conf import settings -def _get_block_size(key_obj, is_encrypt=True) -> int: +def get_default_asymmetric_key_config(cipher_type: str) -> AsymmetricKeyConfig: """ - 获取加解密最大片长度,用于分割过长的文本,单位:bytes - :param key_obj: - :param is_encrypt: + 获取项目默认非对称加密配置 + :param cipher_type: :return: """ - block_size = Util.number.size(key_obj.n) / 8 - reserve_size = 11 - if not is_encrypt: - reserve_size = 0 - return int(block_size - reserve_size) + if cipher_type == crypto_constants.AsymmetricCipherType.SM2.value: + private_key_string: str = settings.SM2_PRIV_KEY + public_key_string: str = json.loads(f'"{settings.SM2_PUB_KEY}"') + elif cipher_type == crypto_constants.AsymmetricCipherType.RSA.value: + private_key_string: str = settings.RSA_PRIV_KEY + public_key_string: str = json.loads(f'"{settings.RSA_PUB_KEY}"') + else: + raise NotImplementedError(f"cipher_type -> {cipher_type}") -def _block_list(lst, block_size): + return AsymmetricKeyConfig( + private_key_string=private_key_string.strip("\n"), public_key_string=public_key_string.strip("\n") + ) + + +def get_default_symmetric_key_config(cipher_type: str) -> SymmetricKeyConfig: """ - 序列切片 - :param lst: - :param block_size: + 获取项目默认对称加密配置 + :param cipher_type: :return: """ - for idx in range(0, len(lst), block_size): - yield lst[idx : idx + block_size] + # 统一使用 APP_SECRET 作为对称加密密钥,SDK 会截断,取符合预期的 key length + return SymmetricKeyConfig(key=settings.SECRET_KEY) -def encrypt_auth_key(auth_key, public_key_name, public_key): - """ - @summary: rsa分块加密 - @param auth_key: 待加密的敏感信息 - @param public_key_name: 公钥名称 - @param public_key: 公钥 - """ - public_key_obj = RSA.importKey(public_key) - message_bytes = auth_key.encode(encoding="utf-8") - encrypt_message_bytes = b"" - block_size = _get_block_size(public_key_obj) - cipher = PKCS1_v1_5_cipher.new(public_key_obj) - for block in _block_list(message_bytes, block_size): - encrypt_message_bytes += cipher.encrypt(block) +def decrypt(ciphertext: str, using: typing.Optional[str] = None) -> str: + using = using or "default" + # 1. 尝试根据前缀解密 + plaintext: str = AsymmetricCipherSelector(using=using).decrypt(ciphertext) - encrypt_message = base64.b64encode(public_key_name.encode("utf-8")) + base64.b64encode(encrypt_message_bytes) - return encrypt_message.decode(encoding="utf-8") + # 2. 尝试对明文二次 RSA 解密,用于兼容原逻辑 + try: + # 该校验用于避免非 b64 串 decode 返回空的场景 + # 尝试 base64 解密,如果解密结果是空串,说明 plaintext 为空或者字符非法,说明非密文,直接返回; + # 如果抛出异常,在外层被 catch 后返回 + if not base64.b64decode(plaintext.encode(encoding="utf-8")): + return plaintext + plaintext = asymmetric_cipher_manager.cipher(using=using, cipher_type=AsymmetricCipherType.RSA.value).decrypt( + plaintext + ) + except Exception: + # 已经是明文的情况下会抛出该异常 + pass + return plaintext -def decrypt_auth_key(encrypt_message, private_key): - """ - @summary: rsa分块解密 - @param encrypt_message: 密文 - @param private_key: rsa私钥 - @return: 解密后的信息 - """ - decrypt_message_bytes = b"" - private_key_obj = RSA.importKey(private_key.strip("\n")) - encrypt_message_bytes = base64.b64decode(encrypt_message) - block_size = _get_block_size(private_key_obj, is_encrypt=False) - cipher = PKCS1_v1_5_cipher.new(private_key_obj) - for block in _block_list(encrypt_message_bytes, block_size): - decrypt_message_bytes += cipher.decrypt(block, "") - return decrypt_message_bytes.decode(encoding="utf-8") + +def encrypt(plaintext: str, using: typing.Optional[str] = None) -> str: + using = using or "default" + return AsymmetricCipherSelector(using=using).encrypt(plaintext) diff --git a/pipeline_plugins/cmdb_ip_picker/query.py b/pipeline_plugins/cmdb_ip_picker/query.py index 27c2e7e07f..d77aa3f9ff 100644 --- a/pipeline_plugins/cmdb_ip_picker/query.py +++ b/pipeline_plugins/cmdb_ip_picker/query.py @@ -16,24 +16,23 @@ import ujson as json from django.http import JsonResponse from django.utils.translation import ugettext_lazy as _ - -from api.utils.request import batch_request from iam.contrib.http import HTTP_AUTH_FORBIDDEN_CODE from iam.exceptions import RawAuthFailedException +from api.utils.request import batch_request from gcloud.conf import settings from gcloud.utils import cmdb -from gcloud.utils.ip import format_sundry_ip from gcloud.utils.handlers import handle_api_error +from gcloud.utils.ip import format_sundry_ip +from .constants import ERROR_CODES, NO_ERROR from .utils import ( get_cmdb_topo_tree, - get_objects_of_topo_tree, - get_modules_of_bk_obj, + get_gse_agent_status_ipv6, get_modules_id, - get_ges_agent_status_ipv6, + get_modules_of_bk_obj, + get_objects_of_topo_tree, ) -from .constants import NO_ERROR, ERROR_CODES logger = logging.getLogger("root") get_client_by_user = settings.ESB_GET_CLIENT_BY_USER @@ -63,8 +62,8 @@ def cmdb_search_host(request, bk_biz_id, bk_supplier_account="", bk_supplier_id= @return: """ default_host_fields = ["bk_host_id", "bk_host_name", "bk_cloud_id", "bk_host_innerip"] - if settings.ENABLE_IPV6: - # IPV6环境下才会尝试去拿agent主机 + if settings.ENABLE_IPV6 or settings.ENABLE_GSE_V2: + # IPV6环境下或者开启了GSE 2.0 版本 default_host_fields.append("bk_agent_id") fields = set(default_host_fields + json.loads(request.GET.get("fields", "[]"))) client = get_client_by_user(request.user.username) @@ -130,7 +129,7 @@ def cmdb_search_host(request, bk_biz_id, bk_supplier_account="", bk_supplier_id= data.append(host_detail) if "agent" in fields: - if settings.ENABLE_IPV6: + if settings.ENABLE_IPV6 or settings.ENABLE_GSE_V2: # 开启IPV6将会调用网关进行查询 bk_agent_id_list = [] for host in data: @@ -144,7 +143,7 @@ def cmdb_search_host(request, bk_biz_id, bk_supplier_account="", bk_supplier_id= bk_agent_id_list.append(bk_agent_id) try: - agent_id_status_map = get_ges_agent_status_ipv6(bk_agent_id_list) + agent_id_status_map = get_gse_agent_status_ipv6(bk_agent_id_list) except Exception as e: result = {"result": False, "code": ERROR_CODES.API_GSE_ERROR, "message": e} return JsonResponse(result) @@ -156,6 +155,7 @@ def cmdb_search_host(request, bk_biz_id, bk_supplier_account="", bk_supplier_id= if not host["bk_host_innerip"]: # 如果既没有如果bk_agent_id,又没有ipv4地址,说明这个主机石台没有安装agent的ipv6主机,忽略,不再查询agent状态, 直接重置为未知 host["agent"] = -1 + continue bk_agent_id = "{}:{}".format(host["bk_cloud_id"], host["bk_host_innerip"]) host["agent"] = agent_id_status_map.get(bk_agent_id, -1) else: diff --git a/pipeline_plugins/cmdb_ip_picker/utils.py b/pipeline_plugins/cmdb_ip_picker/utils.py index 2fc69c6576..daafef025f 100644 --- a/pipeline_plugins/cmdb_ip_picker/utils.py +++ b/pipeline_plugins/cmdb_ip_picker/utils.py @@ -251,6 +251,7 @@ def _inject_topo_params(self, topo_list): self.property_filters["module_property_filter"]["rules"].append( {"field": "bk_module_id", "operator": "in", "value": list(module_ids)} ) + return module_ids def dispatch(self, params): handle_func = getattr(self, f"{self.selector}_picker_handler") @@ -272,7 +273,10 @@ def topo_picker_handler(self, inputted_topo): topo选择情况 :params inputted_topo: 拓扑结构信息列表, list """ - self._inject_topo_params(inputted_topo) + module_ids = self._inject_topo_params(inputted_topo) + if not module_ids: + logger.warning(f"[topo_picker_handler] no module_ids, inputted_topo: {inputted_topo}") + return {"result": True, "data": [], "message": ""} host_info_result = self.fetch_host_ip_with_property_filter() if not host_info_result["result"]: return host_info_result @@ -706,30 +710,45 @@ def get_bk_cloud_id_for_host(host_info, cloud_key="cloud"): return host_info[cloud_key][0]["id"] -def get_ges_agent_status_ipv6(bk_agent_id_list): +def get_gse_agent_status_ipv6(bk_agent_id_list): if not bk_agent_id_list: return {} - ENV_MAP = {"PRODUCT": "prod", "STAGING": "stag"} + ENV_MAP = {"PRODUCT": "prod", "STAGING": "stage"} gse_url = settings.BK_API_URL_TMPL.format(api_name="bk-gse") get_agent_status_url = "{}/{}/api/v2/cluster/list_agent_state".format( - gse_url, ENV_MAP.get(settings.RUN_MODE, "stag") + gse_url, ENV_MAP.get(settings.RUN_MODE, "stage") ) - params = {"bk_app_code": settings.APP_CODE, "bk_app_secret": settings.SECRET_KEY, "agent_id_list": bk_agent_id_list} - resp = requests.post(url=get_agent_status_url, json=params) + def send_request(agent_ids): + params = { + "bk_app_code": settings.APP_CODE, + "bk_app_secret": settings.SECRET_KEY, + "agent_id_list": agent_ids, + } + + resp = requests.post(url=get_agent_status_url, json=params) + + if resp.status_code != 200: + raise Exception("[get_gse_agent_status_ipv6] 查询agent状态错误,返回值非200, content = {}".format(resp.content)) + try: + resp_data = resp.json() + except Exception as e: + raise Exception("[get_gse_agent_status_ipv6] 查询agent状态错误,返回值非Json, err={}".format(e)) + if resp_data["code"] != 0: + raise Exception("[get_gse_agent_status_ipv6] 查询agent状态错误,返回值非code非0, {}".format(data)) + + return resp_data.get("data", []) + + # gse 请求最大支持1000个agent_id的同时查询,所以需要把agent_id分成1000份的单元 + multi_agent_id_list = [bk_agent_id_list[i : i + 1000] for i in range(0, len(bk_agent_id_list), 1000)] - if resp.status_code != 200: - raise Exception("[get_ges_agent_status_ipv6] 查询agent状态错误,返回值非200, content = {}".format(resp.content)) - try: - data = resp.json() - except Exception as e: - raise Exception("[get_ges_agent_status_ipv6] 查询agent状态错误,返回值非Json, err={}".format(e)) - if data["code"] != 0: - raise Exception("[get_ges_agent_status_ipv6] 查询agent状态错误,返回值非code非0") + data = [] + for agent_id_list in multi_agent_id_list: + data.extend(send_request(agent_id_list)) agent_id_status_map = {} - for item in data.get("data", []): + for item in data: # esb agent 状态规则 : agent在线状态,0为不在线,1为在线 # apigw agent 状态规则: Agent当前运行状态码, -1:未知 0:初始安装 1:启动中 2:运行中 3:有损状态 4:繁忙状态 5:升级中 6:停止中 7:解除安装 # 为了前端的显示/与过滤保持一致,所有需要对状态进行转换 diff --git a/pipeline_plugins/components/collections/sites/open/job/all_biz_execute_job_plan/base_service.py b/pipeline_plugins/components/collections/sites/open/job/all_biz_execute_job_plan/base_service.py index 67ca542d08..3a67b8b6f4 100644 --- a/pipeline_plugins/components/collections/sites/open/job/all_biz_execute_job_plan/base_service.py +++ b/pipeline_plugins/components/collections/sites/open/job/all_biz_execute_job_plan/base_service.py @@ -5,26 +5,24 @@ from django.utils import translation from django.utils.translation import ugettext_lazy as _ +from pipeline.core.flow.io import ArrayItemSchema, IntItemSchema, ObjectItemSchema, StringItemSchema + from gcloud.conf import settings from gcloud.constants import JobBizScopeType +from gcloud.utils import crypto from gcloud.utils.handlers import handle_api_error from pipeline_plugins.base.utils.inject import supplier_account_for_business from pipeline_plugins.components.collections.sites.open.job import Jobv3Service from pipeline_plugins.components.collections.sites.open.job.ipv6_base import GetJobTargetServerMixin -from pipeline.core.flow.io import ( - StringItemSchema, - IntItemSchema, - ArrayItemSchema, - ObjectItemSchema, -) +from pipeline_plugins.components.query.sites.open.job import JOBV3_VAR_CATEGORY_IP, JOBV3_VAR_CATEGORY_PASSWORD from pipeline_plugins.components.utils import ( get_job_instance_url, get_node_callback_url, + has_biz_set, loose_strip, + parse_passwd_value, plat_ip_reg, - has_biz_set, ) -from pipeline_plugins.components.query.sites.open.job import JOBV3_VAR_CATEGORY_IP __group_name__ = _("作业平台(JOB)") @@ -126,8 +124,7 @@ def execute(self, data, parent_data): self.biz_scope_type = JobBizScopeType.BIZ.value for _value in original_global_var: - # 3-IP - val = loose_strip(_value["value"]) + val = loose_strip(crypto.decrypt(parse_passwd_value(_value["value"]))) if _value["type"] == JOBV3_VAR_CATEGORY_IP: ip_list = self.get_ip_list(val) @@ -141,6 +138,9 @@ def execute(self, data, parent_data): if result: global_var_list.append({"id": _value["id"], "server": server}) + # 密文变量在没有修改的情况下不加入全局变量,避免脱敏字符串作为正式值进行作业执行逻辑 + elif _value.get("category") == JOBV3_VAR_CATEGORY_PASSWORD and val == "******": + continue else: global_var_list.append({"id": _value["id"], "value": val}) diff --git a/pipeline_plugins/components/collections/sites/open/job/base.py b/pipeline_plugins/components/collections/sites/open/job/base.py index 7b33e3f2a6..0861b6a00a 100644 --- a/pipeline_plugins/components/collections/sites/open/job/base.py +++ b/pipeline_plugins/components/collections/sites/open/job/base.py @@ -350,7 +350,7 @@ def get_job_tagged_ip_dict_complex( step_instance = result["data"]["step_instance_list"][-1] - step_ip_result_list = step_instance["step_ip_result_list"] + step_ip_result_list = step_instance.get("step_ip_result_list", []) success_tags_dict = {} success_ips = [] diff --git a/pipeline_plugins/components/collections/sites/open/job/execute_task/execute_task_base.py b/pipeline_plugins/components/collections/sites/open/job/execute_task/execute_task_base.py index 51a80a361a..31eec55d9b 100644 --- a/pipeline_plugins/components/collections/sites/open/job/execute_task/execute_task_base.py +++ b/pipeline_plugins/components/collections/sites/open/job/execute_task/execute_task_base.py @@ -19,10 +19,17 @@ from pipeline.core.flow.io import ArrayItemSchema, IntItemSchema, ObjectItemSchema, StringItemSchema from gcloud.conf import settings +from gcloud.utils import crypto from gcloud.utils.handlers import handle_api_error from pipeline_plugins.components.collections.sites.open.job import JobService from pipeline_plugins.components.collections.sites.open.job.ipv6_base import GetJobTargetServerMixin -from pipeline_plugins.components.utils import get_job_instance_url, get_node_callback_url, loose_strip +from pipeline_plugins.components.query.sites.open.job import JOBV3_VAR_CATEGORY_IP, JOBV3_VAR_CATEGORY_PASSWORD +from pipeline_plugins.components.utils import ( + get_job_instance_url, + get_node_callback_url, + loose_strip, + parse_passwd_value, +) __group_name__ = _("作业平台(JOB)") @@ -149,9 +156,9 @@ def execute(self, data, parent_data): biz_across = data.get_one_of_inputs("biz_across") for _value in original_global_var: - val = loose_strip(_value["value"]) + val = loose_strip(crypto.decrypt(parse_passwd_value(_value["value"]))) # category为3,表示变量类型为IP - if _value["category"] == 3: + if _value["category"] == JOBV3_VAR_CATEGORY_IP: self.logger.info("[job_execute_task_base] start find ip, var={}".format(val)) if val: server = self.build_ip_list(biz_across, val, executor, biz_cc_id, data, ip_is_exist) @@ -160,6 +167,9 @@ def execute(self, data, parent_data): data.outputs.ex_data = _(f"无法从配置平台(CMDB)查询到对应 IP,请确认输入的 IP 是否合法。查询失败 IP: {val}") return False global_vars.append({"name": _value["name"], "server": server}) + # 密文变量在没有修改的情况下不加入全局变量,避免脱敏字符串作为正式值进行作业执行逻辑 + elif _value.get("category") == JOBV3_VAR_CATEGORY_PASSWORD and val == "******": + continue else: global_vars.append({"name": _value["name"], "value": val}) job_kwargs = { diff --git a/pipeline_plugins/components/collections/sites/open/nodeman/base.py b/pipeline_plugins/components/collections/sites/open/nodeman/base.py index 778f7bf9f5..82b8059561 100644 --- a/pipeline_plugins/components/collections/sites/open/nodeman/base.py +++ b/pipeline_plugins/components/collections/sites/open/nodeman/base.py @@ -10,7 +10,13 @@ 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 base64 + import ujson as json +from bkcrypto.asymmetric.ciphers import BaseAsymmetricCipher +from bkcrypto.asymmetric.interceptors import BaseAsymmetricInterceptor +from bkcrypto.constants import AsymmetricCipherType +from bkcrypto.contrib.django.ciphers import get_asymmetric_cipher from django.utils.translation import ugettext_lazy as _ from pipeline.core.flow.activity import Service, StaticIntervalGenerator from pipeline.core.flow.io import ArrayItemSchema, IntItemSchema, ObjectItemSchema, StringItemSchema @@ -25,6 +31,15 @@ from pipeline_plugins.components.utils.sites.open.utils import get_nodeman_job_url +class NodeManAsymmetricInterceptor(BaseAsymmetricInterceptor): + + PREFIX: str = base64.b64encode("DEFAULT".encode()).decode(encoding="utf-8") + + @classmethod + def after_encrypt(cls, ciphertext: str, **kwargs) -> str: + return f"{cls.PREFIX}{ciphertext}" + + def get_host_id_by_inner_ip(executor, logger, bk_cloud_id: int, bk_biz_id: int, ip_list: list): """ 根据inner_ip获取bk_host_id 对应关系dict @@ -71,19 +86,28 @@ def get_host_id_by_inner_ipv6(executor, logger, bk_cloud_id: int, bk_biz_id: int return {host["inner_ipv6"]: host["bk_host_id"] for host in result["data"]["list"]} -def get_nodeman_rsa_public_key(executor, logger): +def get_nodeman_public_key(executor, logger): """ 拉取节点管理rsa公钥 """ client = BKNodeManClient(username=executor) - get_rsa_result = client.get_rsa_public_key(executor) - if not get_rsa_result["result"]: - error = handle_api_error(__group_name__, "nodeman.get_rsa_public_key", executor, get_rsa_result) + pub_key_response = client.get_rsa_public_key(executor) + if not pub_key_response["result"]: + error = handle_api_error(__group_name__, "nodeman.get_rsa_public_key", executor, pub_key_response) logger.error(error) return False, None - content = get_rsa_result["data"][0]["content"] - name = get_rsa_result["data"][0]["name"] - return True, {"name": name, "content": content} + try: + public_key_info = pub_key_response["data"][0] + except (AssertionError, IndexError): + logger.error("fetch_public_keys return empty data") + return False, None + + return True, { + "name": public_key_info["name"], + "content": public_key_info["content"], + # 如果没有返回 cipher_type,说明本环境节点管理还没更新为国密适配版本,此时设置 cipher_type 为 RSA,向前兼容节点管理的接口行为 + "cipher_type": public_key_info.get("cipher_type") or AsymmetricCipherType.RSA.value, + } class NodeManBaseService(Service): @@ -149,6 +173,20 @@ def get_host_id_list(self, ip_str, executor, bk_cloud_id, bk_biz_id): bk_host_id_dict = get_host_id_by_inner_ip(executor, self.logger, bk_cloud_id, bk_biz_id, ip_list) return [bk_host_id for bk_host_id in bk_host_id_dict.values()] + def parse2nodeman_ciphertext(self, data, executor, plaintext) -> str: + success, public_key_info = get_nodeman_public_key(executor, self.logger) + if not success: + data.set_outputs("ex_data", _("获取节点管理公钥失败,请查看节点日志获取错误详情.")) + raise ValueError + + # 根据接口的 cipher type 和 publickey 创建 cipher + cipher: BaseAsymmetricCipher = get_asymmetric_cipher( + cipher_type=public_key_info["cipher_type"], + common={"public_key_string": public_key_info["content"], "interceptor": NodeManAsymmetricInterceptor}, + ) + + return cipher.encrypt(plaintext) + def outputs_format(self): return [ self.OutputItem( diff --git a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v2_0.py b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v2_0.py index 89adb8d22c..b4aac307aa 100644 --- a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v2_0.py +++ b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v2_0.py @@ -18,15 +18,17 @@ from api.collections.nodeman import BKNodeManClient from gcloud.conf import settings -from gcloud.utils.crypto import decrypt_auth_key, encrypt_auth_key +from gcloud.utils import crypto from pipeline_plugins.components.collections.sites.open.nodeman.base import ( NodeManBaseService, get_host_id_by_inner_ip, get_host_id_by_inner_ipv6, - get_nodeman_rsa_public_key, ) __group_name__ = _("节点管理(Nodeman)") + +from pipeline_plugins.components.utils import parse_passwd_value + VERSION = "v2.0" # 安装类任务(job_install) @@ -55,15 +57,6 @@ def execute(self, data, parent_data): bk_cloud_id = nodeman_op_target.get("nodeman_bk_cloud_id", "") node_type = nodeman_op_target.get("nodeman_node_type", "") - nodeman_ticket = data.get_one_of_inputs("nodeman_ticket", {}) - nodeman_tjj_ticket = nodeman_ticket.get("nodeman_tjj_ticket", "") - if nodeman_tjj_ticket: - try: - nodeman_tjj_ticket = decrypt_auth_key(nodeman_tjj_ticket, settings.RSA_PRIV_KEY) - except Exception: - # password is not encrypted - pass - nodeman_op_info = data.inputs.nodeman_op_info op_type = nodeman_op_info.get("nodeman_op_type", "") nodeman_hosts = nodeman_op_info.get("nodeman_hosts", []) @@ -105,7 +98,6 @@ def execute(self, data, parent_data): all_hosts, row_host_params_list = [], [] for host in nodeman_hosts: auth_type = host["auth_type"] - auth_key = host["auth_key"] use_inner_ip = True if host.get("inner_ip") else False # use_inner_ip 判定用户输入的的是ipv4还是ipv6 inner_ip_list = self.get_ip_list( @@ -118,17 +110,12 @@ def execute(self, data, parent_data): return False # 处理表格中每行的key/psw + auth_key: str = crypto.decrypt(parse_passwd_value(host["auth_key"])) try: - auth_key = decrypt_auth_key(auth_key, settings.RSA_PRIV_KEY) - except Exception: - # password is not encrypted - pass - # auth_key加密 - success, ras_public_key = get_nodeman_rsa_public_key(executor, self.logger) - if not success: - data.set_outputs("ex_data", _("获取节点管理公钥失败,请查看节点日志获取错误详情.")) + auth_key: str = self.parse2nodeman_ciphertext(data, executor, auth_key) + except ValueError: return False - auth_key = encrypt_auth_key(auth_key, ras_public_key["name"], ras_public_key["content"]) + # 表格每行基础参数 base_params = { "bk_biz_id": bk_biz_id, @@ -188,8 +175,6 @@ def execute(self, data, parent_data): kwargs = {"job_type": job_name, "hosts": all_hosts, "action": "job_install"} - if nodeman_tjj_ticket: - kwargs.update({"tcoa_ticket": nodeman_tjj_ticket}) else: data.set_outputs("ex_data", _("无效的操作请求:{}".format(job_name))) return False @@ -202,7 +187,10 @@ def execute(self, data, parent_data): def inputs_format(self): return [ self.InputItem( - name=_("业务 ID"), key="bk_biz_id", type="int", schema=IntItemSchema(description=_("当前操作所属的 CMDB 业务 ID")), + name=_("业务 ID"), + key="bk_biz_id", + type="int", + schema=IntItemSchema(description=_("当前操作所属的 CMDB 业务 ID")), ), self.InputItem( name=_("操作对象"), diff --git a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v3_0.py b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v3_0.py index c3c0cb514a..518ed9ff03 100644 --- a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v3_0.py +++ b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v3_0.py @@ -17,15 +17,17 @@ from api.collections.nodeman import BKNodeManClient from gcloud.conf import settings -from gcloud.utils.crypto import decrypt_auth_key, encrypt_auth_key +from gcloud.utils import crypto from pipeline_plugins.components.collections.sites.open.nodeman.base import ( NodeManNewBaseService, get_host_id_by_inner_ip, get_host_id_by_inner_ipv6, - get_nodeman_rsa_public_key, ) __group_name__ = _("节点管理(Nodeman)") + +from pipeline_plugins.components.utils import parse_passwd_value + VERSION = "v3.0" # 安装类任务(job_install) @@ -98,7 +100,6 @@ def execute(self, data, parent_data): bk_cloud_id = host["nodeman_bk_cloud_id"] ap_id = host["nodeman_ap_id"] auth_type = host["auth_type"] - auth_key = host["auth_key"] use_inner_ip = True if host.get("inner_ip") else False # use_inner_ip 判定用户输入的的是ipv4还是ipv6 inner_ip_list = self.get_ip_list( @@ -112,17 +113,12 @@ def execute(self, data, parent_data): return False # 处理表格中每行的key/psw + auth_key: str = crypto.decrypt(parse_passwd_value(host["auth_key"])) try: - auth_key = decrypt_auth_key(auth_key, settings.RSA_PRIV_KEY) - except Exception: - # password is not encrypted - pass - # auth_key加密 - success, ras_public_key = get_nodeman_rsa_public_key(executor, self.logger) - if not success: - data.set_outputs("ex_data", _("获取节点管理公钥失败,请查看节点日志获取错误详情.")) + auth_key: str = self.parse2nodeman_ciphertext(data, executor, auth_key) + except ValueError: return False - auth_key = encrypt_auth_key(auth_key, ras_public_key["name"], ras_public_key["content"]) + # 表格每行基础参数 base_params = { "bk_biz_id": bk_biz_id, diff --git a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v4_0.py b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v4_0.py index c07311b5d8..6416ae60d0 100644 --- a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v4_0.py +++ b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v4_0.py @@ -17,15 +17,15 @@ from api.collections.nodeman import BKNodeManClient from gcloud.conf import settings +from gcloud.utils import crypto from gcloud.utils.cmdb import get_business_host, get_business_host_ipv6 -from gcloud.utils.crypto import decrypt_auth_key, encrypt_auth_key from pipeline_plugins.base.utils.inject import supplier_account_for_business -from pipeline_plugins.components.collections.sites.open.nodeman.base import ( - NodeManNewBaseService, - get_nodeman_rsa_public_key, -) +from pipeline_plugins.components.collections.sites.open.nodeman.base import NodeManNewBaseService __group_name__ = _("节点管理(Nodeman)") + +from pipeline_plugins.components.utils import parse_passwd_value + VERSION = "v4.0" # 无需认证信息的操作 @@ -37,12 +37,7 @@ # 依赖节点管理 job/install 接口的操作 INSTALL_JOB = ( - [ - "INSTALL_PROXY", - "INSTALL_AGENT", - "REINSTALL_PROXY", - "REINSTALL_AGENT", - ] + ["INSTALL_PROXY", "INSTALL_AGENT", "REINSTALL_PROXY", "REINSTALL_AGENT"] + NOT_NEED_EXTRA_CONFIG_JOB + NOT_NEED_AUTH_JOB ) @@ -110,28 +105,15 @@ def execute(self, data, parent_data): # 认证信息 auth_params.update( - { - "port": host["port"], - "auth_type": host["auth_type"], - "account": host["account"], - } + {"port": host["port"], "auth_type": host["auth_type"], "account": host["account"]} ) - auth_key = host["auth_key"] - # 处理表格中每行的key/psw + auth_key: str = crypto.decrypt(parse_passwd_value(host["auth_key"])) try: - auth_key = decrypt_auth_key(auth_key, settings.RSA_PRIV_KEY) - except Exception: - self.logger.info("try to decrypt password error, use plaintext.") - pass - - # auth_key加密 - success, ras_public_key = get_nodeman_rsa_public_key(executor, self.logger) - if not success: - data.set_outputs("ex_data", _("获取节点管理公钥失败,请查看节点日志获取错误详情.")) + auth_key: str = self.parse2nodeman_ciphertext(data, executor, auth_key) + except ValueError: return False - auth_key = encrypt_auth_key(auth_key, ras_public_key["name"], ras_public_key["content"]) if auth_params["auth_type"] == "PASSWORD": auth_params["password"] = auth_key diff --git a/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v5_0.py b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v5_0.py new file mode 100644 index 0000000000..ad8d448e0e --- /dev/null +++ b/pipeline_plugins/components/collections/sites/open/nodeman/create_task/v5_0.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017 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 +http://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 django.utils.translation import ugettext_lazy as _ +from pipeline.component_framework.component import Component + +from gcloud.conf import settings + +__group_name__ = _("节点管理(Nodeman)") + +from pipeline_plugins.components.collections.sites.open.nodeman.create_task.v4_0 import ( + NodemanCreateTaskService as NodemanCreateTaskV4Service, +) + +VERSION = "v5.0" + + +class NodemanCreateTaskService(NodemanCreateTaskV4Service): + pass + + +class NodemanCreateTaskComponent(Component): + name = _("新建任务") + code = "nodeman_create_task" + bound_service = NodemanCreateTaskService + form = "%scomponents/atoms/nodeman/create_task/v5_0.js" % settings.STATIC_URL + version = VERSION + desc = _("v5.0版本 支持密码脱敏填写") diff --git a/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v1_0.py b/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v1_0.py index e14ccbcf94..1d00698457 100644 --- a/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v1_0.py +++ b/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v1_0.py @@ -15,15 +15,18 @@ import requests from django.utils.translation import ugettext_lazy as _ - -from pipeline.core.flow.activity import Service from pipeline.component_framework.component import Component from pipeline.conf import settings +from pipeline.core.flow.activity import Service from pipeline.core.flow.io import StringItemSchema + from gcloud.core.models import EnvironmentVariables __group_name__ = _("企业微信(WechatWork)") +from gcloud.utils import crypto +from pipeline_plugins.components.utils import parse_passwd_value + class WechatWorkSendMessageService(Service): def inputs_format(self): @@ -35,7 +38,10 @@ def inputs_format(self): schema=StringItemSchema(description=_("通过在群里@企业微信机器人获取,多个用换行分隔")), ), self.InputItem( - name=_("消息内容"), key="message_content", type="string", schema=StringItemSchema(description=_("消息内容")), + name=_("消息内容"), + key="message_content", + type="string", + schema=StringItemSchema(description=_("消息内容")), ), self.InputItem( name=_("提醒人"), @@ -57,8 +63,13 @@ def execute(self, data, parent_data): mentioned_members = data.inputs.wechat_work_mentioned_members msgtype = data.get_one_of_inputs("msgtype", "text") + # 尝试解密 + chat_id: str = crypto.decrypt(parse_passwd_value(chat_id)) + chat_id_list = chat_id.split("\n") + print(chat_id_list) + url = EnvironmentVariables.objects.get_var("BKAPP_SOPS_WECHAT_WORK_WEB_HOOK") if not url: data.outputs.ex_data = "WechatWork send message URL is not config, contact admin please" @@ -68,9 +79,9 @@ def execute(self, data, parent_data): data.outputs.ex_data = _("会话 ID 不能为空") return False - for c in chat_id_list: + for idx, c in enumerate(chat_id_list, 1): if len(c) != 32: - data.outputs.ex_data = _("无效的会话 ID: {}".format(c)) + data.outputs.ex_data = _("第{idx}行的会话 ID 格式不正确(长度需为 32)".format(idx=idx)) return False mentioned_list = [] diff --git a/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v2_0.py b/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v2_0.py new file mode 100644 index 0000000000..07cb444d55 --- /dev/null +++ b/pipeline_plugins/components/collections/sites/open/wechat_work/wechat_work_send_message/v2_0.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017 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 +http://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 django.utils.translation import ugettext_lazy as _ +from pipeline.component_framework.component import Component +from pipeline.conf import settings + +from pipeline_plugins.components.collections.sites.open.wechat_work.wechat_work_send_message import v1_0 + +__group_name__ = _("企业微信(WechatWork)") + + +class WechatWorkSendMessageService(v1_0.WechatWorkSendMessageService): + pass + + +class WechatWorkSendMessageComponent(Component): + name = _("发送消息") + code = "wechat_work_send_message" + bound_service = WechatWorkSendMessageService + form = "%scomponents/atoms/wechat_work/wechat_work_send_message/v2_0.js" % settings.STATIC_URL + version = "2.0" + desc = _( + "1.部署环境与企业微信服务器网络必须联通 " + "2.通过企业微信机器人获取会话 ID,可参考https://open.work.weixin.qq.com/api/doc/90000/90136/91770" + "3. 支持「会话 ID」脱敏填写" + ) diff --git a/pipeline_plugins/components/static/components/atoms/job/execute_task/v1_0.js b/pipeline_plugins/components/static/components/atoms/job/execute_task/v1_0.js index 0fc7c110c6..4142a6f4b1 100644 --- a/pipeline_plugins/components/static/components/atoms/job/execute_task/v1_0.js +++ b/pipeline_plugins/components/static/components/atoms/job/execute_task/v1_0.js @@ -66,6 +66,7 @@ } let biz_cc_id = this.get_parent && this.get_parent().get_child('biz_cc_id')._get_value(); let bk_job_host = window.BK_JOB_HOST; + if (bk_job_host.charAt(bk_job_host.length - 1) == "/") bk_job_host = bk_job_host.substr(0, bk_job_host.length - 1); let url = bk_job_host + "/api_plan/" + this.value; window.open(url, '_blank') }, diff --git a/pipeline_plugins/components/static/components/atoms/job/job_execute_task.js b/pipeline_plugins/components/static/components/atoms/job/job_execute_task.js index 8b4876a9c6..c049489e33 100644 --- a/pipeline_plugins/components/static/components/atoms/job/job_execute_task.js +++ b/pipeline_plugins/components/static/components/atoms/job/job_execute_task.js @@ -66,7 +66,8 @@ } let biz_cc_id = this.get_parent && this.get_parent().get_child('biz_cc_id')._get_value(); let bk_job_host = window.BK_JOB_HOST; - let url = bk_job_host + '/' + biz_cc_id + "/api_plan/" + this.value; + if (bk_job_host.charAt(bk_job_host.length - 1) == "/") bk_job_host = bk_job_host.substr(0, bk_job_host.length - 1); + let url = bk_job_host + "/api_plan/" + this.value; window.open(url, '_blank') }, validation: [ diff --git a/pipeline_plugins/components/static/components/atoms/job/job_fast_execute_script.js b/pipeline_plugins/components/static/components/atoms/job/job_fast_execute_script.js index 4ed34c0891..216248e41d 100644 --- a/pipeline_plugins/components/static/components/atoms/job/job_fast_execute_script.js +++ b/pipeline_plugins/components/static/components/atoms/job/job_fast_execute_script.js @@ -99,7 +99,7 @@ REM 可在脚本执行失败的逻辑分支处调用,打印当时的时间戳 exit 1 `) } else if (language === "3") { - return gettext(#!/usr/bin/perl + return gettext(`#!/usr/bin/perl use strict; @@ -131,7 +131,7 @@ job_start; ###### 如果返回值为0,则认为此脚本执行成功,如果非0,则认为脚本执行失败 ###### 可在此处开始编写您的脚本逻辑代码 -) +`) } else if (language === "4") { return gettext(`#!/usr/bin/env python # -*- coding: utf8 -*- diff --git a/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v4_0.js b/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v4_0.js index de8d830beb..ba487b09f4 100644 --- a/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v4_0.js +++ b/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v4_0.js @@ -25,18 +25,6 @@ function init_columns(self, node_type, op_type) { - - // Proxy 仅支持 Linux - let os_type_options = [ - {value: "LINUX", text: gettext("LINUX")} - ]; - if (node_type === "AGENT") { - os_type_options.push(...[ - {value: "WINDOWS", text: gettext("WINDOWS")}, - {value: "AIX", text: gettext("AIX")} - ]); - } - let common_columns = [ { tag_code: "nodeman_bk_cloud_id", @@ -105,7 +93,11 @@ attrs: { name: gettext("操作系统类型"), width: "180px", - items: os_type_options, + items: [ + {value: "LINUX", text: gettext("LINUX")}, + {value: "WINDOWS", text: gettext("WINDOWS")}, + {value: "AIX", text: gettext("AIX")} + ], default: "LINUX", validation: [ { @@ -299,7 +291,6 @@ if (op_type !== "UNINSTALL") { self.columns.push(...config_columns); } - console.log(node_type, op_type); }; let NODEMAN_TJJ_IS_HIDDEN = true; diff --git a/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v5_0.js b/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v5_0.js new file mode 100644 index 0000000000..cd9888e61e --- /dev/null +++ b/pipeline_plugins/components/static/components/atoms/nodeman/create_task/v5_0.js @@ -0,0 +1,619 @@ +/** + * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community + * Edition) available. + * Copyright (C) 2017-2020 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 + * http://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. + */ + +(function () { + function is_display_tag(self, op_type, value) { + if (op_type.indexOf(value) !== -1) { + self.show(); + } else { + self.hide(); + } + } + + function is_install_op(self, value) { + is_display_tag(self, ["INSTALL", "REINSTALL"], value); + } + + + function init_columns(self, node_type, op_type) { + let common_columns = [ + { + tag_code: "nodeman_bk_cloud_id", + type: "select", + attrs: { + name: gettext("管控区域ID"), + width: "180px", + remote: true, + items: [], + remote_url: $.context.get("site_url") + "pipeline/nodeman_get_cloud_area/", + remote_data_init: function (resp) { + if (resp.result === false) { + show_msg(resp.message, "error"); + } + resp.data.unshift({"text": gettext("直连区域"), "value": 0}); + return resp.data; + }, + validation: [ + { + type: "required", + } + ] + } + }, + { + tag_code: "nodeman_ap_id", + type: "select", + attrs: { + name: gettext("接入点"), + width: "180px", + remote: true, + items: [], + remote_url: $.context.get("site_url") + "pipeline/nodeman_get_ap_list/", + remote_data_init: function (resp) { + if (resp.result === false) { + show_msg(resp.message, "error"); + } + return resp.data; + }, + validation: [] + } + }, + { + tag_code: "inner_ip", + type: "textarea", + attrs: { + name: gettext("内网IP"), + placeholder: gettext("多个用英文逗号 `,` 或换行分隔"), + width: "180px", + editable: true + } + }, + { + tag_code: "inner_ipv6", + type: "textarea", + attrs: { + name: gettext("内网IP(IPV6)"), + placeholder: gettext("可为空,如纯ipv6主机,内网ipv和外网IP(IPV6)两个必须填一个"), + width: "180px", + editable: true, + } + }, + { + tag_code: "os_type", + type: "select", + attrs: { + name: gettext("操作系统类型"), + width: "180px", + items: [ + {value: "LINUX", text: gettext("LINUX")}, + {value: "WINDOWS", text: gettext("WINDOWS")}, + {value: "AIX", text: gettext("AIX")} + ], + default: "LINUX", + validation: [ + { + type: "required" + } + ], + } + }, + ]; + + let proxy_columns = [ + { + tag_code: "outer_ip", + type: "textarea", + attrs: { + name: gettext("外网IP"), + placeholder: gettext("可选,如填写需与内网ip一一对应,多个用英文逗号 `,` 或换行分隔"), + width: "180px", + editable: true, + validation: [] + }, + }, + { + tag_code: "data_ip", + type: "textarea", + attrs: { + name: gettext("数据IP"), + placeholder: gettext("可为空,如填写需与内网ip一一对应,适配复杂网络时填写,多个用英文逗号 `,` 或换行分隔"), + width: "180px", + editable: true, + } + }, + { + tag_code: "outer_ipv6", + type: "textarea", + attrs: { + name: gettext("外网IP(IPV6)"), + placeholder: gettext("可为空,如纯ipv6主机,外网ipv和外网IP(IPV6)两个必须填一个"), + width: "180px", + editable: true, + }, + }, + ]; + + let auth_columns = [ + { + tag_code: "login_ip", + type: "textarea", + attrs: { + name: gettext("登录IP"), + placeholder: gettext("可为空,如填写需与内网ip一一对应,适配复杂网络时填写,多个用英文逗号 `,` 或换行分隔"), + width: "180px", + editable: true + } + }, + { + tag_code: "account", + type: "input", + attrs: { + name: gettext("登录账号"), + width: "180px", + editable: true, + validation: [ + { + type: "required" + } + ], + } + }, + { + tag_code: "port", + type: "input", + attrs: { + name: gettext("端口号"), + width: "100px", + editable: true, + validation: [ + { + type: "required" + } + ], + } + }, + { + tag_code: "auth_type", + type: "select", + attrs: { + name: gettext("认证方式"), + width: "180px", + items: [ + {value: "PASSWORD", text: gettext("PASSWORD")}, + {value: "KEY", text: gettext("KEY")}, + {value: "TJJ_PASSWORD", text: gettext("TJJ")} + + ], + default: "PASSWORD", + validation: [ + { + type: "required" + } + ], + } + }, + { + tag_code: "auth_key", + type: "password", + attrs: { + name: gettext("认证密钥"), + width: "400px", + editable: true, + textareaMode: true, + validation: [ + { + type: "custom", + args: function (value, parent_value) { + let auth_type = parent_value.auth_type; + let result = { + result: true, + error_message: "" + }; + if (auth_type !== "TJJ_PASSWORD" && !value.value.length) { + result.result = false; + result.error_message = gettext("请输入认证密钥"); + } + return result; + } + } + ] + }, + }, + ]; + + let config_columns = [ + { + tag_code: "peer_exchange_switch_for_agent", + type: "radio", + attrs: { + name: gettext("BT节点探测"), + items: [ + {value: 1, name: gettext("是")}, + {value: 0, name: gettext("否")} + ], + default: 0, + validation: [ + { + type: "required" + }, + ] + }, + }, + { + tag_code: "speed_limit", + type: "input", + attrs: { + name: gettext("传输限速 M/s"), + width: "100px", + placeholder: gettext("请输入"), + validation: [ + { + type: "custom", + args: function (value) { + var result = { + result: true, + error_message: "" + }; + if (value && !Number(value)) { + result.result = false; + result.error_message = gettext("请输入数字"); + } + return result; + } + } + ] + }, + }, + ]; + + self.columns = common_columns; + + // 如果是 Proxy,补充 Proxy 信息 + if (node_type === "PROXY") { + self.columns.push(...proxy_columns); + } + // 安装 / 重装 / 卸载需要认证信息 + if (op_type === "INSTALL" || op_type === "UNINSTALL" || op_type === "REINSTALL") { + self.columns.push(...auth_columns); + } + // 非卸载场景需要配置信息 + if (op_type !== "UNINSTALL") { + self.columns.push(...config_columns); + } + }; + + let NODEMAN_TJJ_IS_HIDDEN = true; + + $.ajax({ + url: $.context.get('site_url') + 'pipeline/nodeman_is_support_tjj/', + type: 'GET', + dataType: 'json', + async: false, + success: function (resp) { + if (resp.data) { + NODEMAN_TJJ_IS_HIDDEN = false; + } + }, + error: function () { + show_msg('request nodeman is support tjj error', 'error'); + } + }); + $.atoms.nodeman_create_task = [ + { + tag_code: "bk_biz_id", + type: "select", + attrs: { + name: gettext("业务"), + allowCreate: true, + hookable: true, + remote: true, + remote_url: $.context.get("site_url") + "pipeline/cc_get_business_list/", + remote_data_init: function (resp) { + if (resp.result === false) { + show_msg(resp.message, 'error'); + } + return resp.data; + }, + disabled: !$.context.canSelectBiz(), + validation: [ + { + type: "required" + } + ] + }, + methods: { + _tag_init: function () { + if (this.value) { + return + } + this._set_value($.context.getBkBizId()); + } + } + }, + { + tag_code: "nodeman_op_info", + type: "combine", + attrs: { + name: gettext("操作类型"), + hookable: true, + children: [ + { + tag_code: "nodeman_node_type", + type: "radio", + attrs: { + name: gettext("节点类型"), + hookable: true, + items: [ + {value: "AGENT", name: gettext("AGENT")}, + {value: "PROXY", name: gettext("PROXY")}, + ], + default: "AGENT", + validation: [ + { + type: "required" + } + ], + events: [ + { + source: "nodeman_node_type", + type: "init", + action: function (value) { + // 统一以 change 事件抛出 + this.emit_event(this.tagCode, "change", this.value); + } + }, + ] + } + }, + { + tag_code: "nodeman_op_type", + type: "select", + attrs: { + name: gettext("操作类型"), + items: [ + {value: "INSTALL", text: gettext("安裝")}, + {value: "REINSTALL", text: gettext("重新安装")}, + {value: "UNINSTALL", text: gettext("卸载")}, + {value: "UPGRADE", text: gettext("升级")}, + {value: "RESTART", text: gettext("重启")}, + {value: "RELOAD", text: gettext("配置重载")}, + ], + default: "INSTALL", + validation: [ + { + type: "required" + } + ] + }, + events: [ + { + source: "nodeman_op_type", + type: "init", + action: function (value) { + this.emit_event(this.tagCode, "change", this.value); + } + }, + ] + }, + { + tag_code: "nodeman_install_latest_plugins", + type: "radio", + attrs: { + name: gettext("是否安装最新版本插件"), + items: [ + {value: true, name: gettext("是")}, + {value: false, name: gettext("否")} + ], + default: true, + validation: [ + { + type: "required" + }, + ] + }, + events: [ + { + source: "nodeman_op_type", + type: "change", + action: function (value) { + is_install_op(this, value); + } + }, + ] + }, + { + tag_code: "nodeman_hosts", + type: "datatable", + attrs: { + pagination: true, + name: gettext("主机"), + table_buttons: [ + { + type: "add_row", + text: gettext("添加"), + callback: function () { + this.add_row(); + } + }, + { + type: "import", + text: gettext("导入") + }, + { + type: "export", + text: gettext("导出"), + callback: function () { + this.export2Excel(); + } + }, + + ], + columns: [], + hookable: true, + validation: [ + { + type: "custom", + args: function (value) { + let self = this; + let result = { + result: true, + error_message: "" + }; + let op_type = self.get_parent && self.get_parent().get_child("nodeman_op_type").value; + let install_type = ["INSTALL", "REINSTALL"]; + if (install_type.indexOf(op_type) !== -1 && value === "") { + result.result = false; + result.error_message = gettext("请完善主机信息"); + } + return result; + + } + } + ] + }, + events: [ + { + source: "nodeman_op_type", + type: "change", + action: function (value) { + let node_type = this.get_parent().get_child("nodeman_node_type").value; + init_columns(this, node_type, value); + let op_type = ["INSTALL", "REINSTALL", "RELOAD"]; + if (node_type === "AGENT") { + op_type = ["INSTALL", "REINSTALL", "UNINSTALL", "RELOAD"]; + } + is_display_tag(this, op_type, value); + } + }, + { + source: "nodeman_node_type", + type: "change", + action: function (value) { + init_columns(this, value, this.get_parent().get_child("nodeman_op_type").value); + let op_type = ["INSTALL", "REINSTALL", "RELOAD"]; + if (value === "AGENT") { + op_type = ["INSTALL", "REINSTALL", "UNINSTALL", "RELOAD"]; + } + is_display_tag(this, op_type, this.get_parent().get_child("nodeman_op_type").value); + } + }, + ] + }, + { + tag_code: "nodeman_other_hosts", + type: "datatable", + attrs: { + pagination: true, + name: gettext("主机"), + table_buttons: [ + { + type: "add_row", + text: gettext("添加"), + callback: function () { + this.add_row(); + } + }, + { + type: "import", + text: gettext("导入") + }, + { + type: "export", + text: gettext("导出"), + callback: function () { + this.export2Excel(); + } + }, + + ], + columns: [ + { + tag_code: "nodeman_bk_cloud_id", + type: "input", + attrs: { + name: gettext("管控区域ID"), + placeholder: gettext("一行只能对应一个管控区域ID"), + hookable: true, + validation: [ + { + type: "required" + } + ] + } + }, + { + tag_code: "nodeman_ip_str", + type: "textarea", + attrs: { + name: gettext("IP"), + placeholder: gettext("多个用英文逗号 `,` 分隔"), + editable: true, + validation: [ + { + type: "required" + } + ] + } + } + ], + hookable: true, + validation: [ + { + type: "custom", + args: function (value) { + let self = this; + let result = { + result: true, + error_message: "" + }; + let op_type = self.get_parent && self.get_parent().get_child("nodeman_op_type").value; + let install_type = ["UNINSTALL", "UPGRADE"]; + if (install_type.indexOf(op_type) !== -1 && value === "") { + result.result = false; + result.error_message = gettext("请完善主机信息"); + } + return result; + + } + } + ] + }, + events: [ + { + source: "nodeman_op_type", + type: "change", + action: function (value) { + let op_type = ["UPGRADE", "RESTART"]; + if (this.get_parent().get_child("nodeman_node_type").value === "PROXY") { + op_type = ["UPGRADE", "RESTART", "UNINSTALL"]; + } + is_display_tag(this, op_type, value); + } + }, + { + source: "nodeman_node_type", + type: "change", + action: function (value) { + let op_type = ["UPGRADE", "RESTART"]; + if (value === "PROXY") { + op_type = ["UPGRADE", "RESTART", "UNINSTALL"]; + } + is_display_tag(this, op_type, this.get_parent().get_child("nodeman_op_type").value); + } + }, + ] + }, + ], + }, + }, + ]; +})(); diff --git a/pipeline_plugins/components/static/components/atoms/wechat_work/wechat_work_send_message/v2_0.js b/pipeline_plugins/components/static/components/atoms/wechat_work/wechat_work_send_message/v2_0.js new file mode 100644 index 0000000000..ca20765c14 --- /dev/null +++ b/pipeline_plugins/components/static/components/atoms/wechat_work/wechat_work_send_message/v2_0.js @@ -0,0 +1,59 @@ +$.atoms.wechat_work_send_message = [ + { + "type": "password", + "attrs": { + "name": gettext("会话ID"), + "hookable": true, + "textareaMode": true, + "validation": [ + { + "type": "required" + } + ], + "placeholder": gettext("通过在群里@企业微信机器人获取,多个用换行分隔"), + }, + "tag_code": "wechat_work_chat_id" + }, + { + "tag_code": "msgtype", + "type": "radio", + "attrs": { + "name": gettext("消息格式"), + "hookable": false, + "items": [ + {"name": gettext("文本(text)"), "value": "text"}, + {"name": gettext("Markdown"), "value": "markdown"}, + ], + "default": "text", + "validation": [ + { + "type": "required" + } + ], + }, + }, + { + "type": "textarea", + "attrs": { + "name": gettext("消息内容"), + "hookable": true, + "validation": [ + { + "type": "required" + } + ] + }, + "tag_code": "message_content" + }, + { + "type": "input", + "attrs": { + "name": gettext("提醒人"), + "hookable": true, + "placeholder": gettext("提醒群指定成员(@某个成员),多个成员用 `,` 分隔,@all表示提醒所有人") + }, + "events": [], + "methods": {}, + "tag_code": "wechat_work_mentioned_members" + } +] \ No newline at end of file diff --git a/pipeline_plugins/components/utils/common.py b/pipeline_plugins/components/utils/common.py index eb29058bea..aa75ddcf78 100644 --- a/pipeline_plugins/components/utils/common.py +++ b/pipeline_plugins/components/utils/common.py @@ -11,15 +11,14 @@ specific language governing permissions and limitations under the License. """ import logging +import random import re import time -import random +import typing from copy import deepcopy from django.utils.translation import ugettext_lazy as _ -from pipeline.conf import settings -from pipeline.utils.crypt import rsa_decrypt_password from api.utils.thread import ThreadPool logger = logging.getLogger("root") @@ -43,18 +42,6 @@ def loose_strip(data): return data -def try_decrypt_password(password): - """ - @summary: 尝试解密操作,成功返回明文密码,错误则说明用户使用明文的密码,返回 - @param password: - @return: - """ - try: - return rsa_decrypt_password(password, settings.RSA_PRIV_KEY) - except Exception: - return password - - def chunk_table_data(column_dict, break_line): """ @summary: 表格参数值支持以break_line为分隔符分隔的多条数据,对一行数据,当有一列有多条数据时(包含换行符),其他列要么也有相等个数的 @@ -132,3 +119,13 @@ def convert_num_to_str(export_data: list): if isinstance(value, int) or isinstance(value, float): data[key] = str(value) return export_data + + +def parse_passwd_value(passwd_value: typing.Union[str, typing.Dict[str, str]]) -> str: + if isinstance(passwd_value, str): + return passwd_value + elif isinstance(passwd_value, dict): + return parse_passwd_value(passwd_value.get("value", "")) + else: + # 仅有两种合法结构,如果都不满足直接返回空串 + return "" diff --git a/pipeline_plugins/tests/components/collections/sites/open/job_test/all_biz_execute_job_plan/test_v1_0.py b/pipeline_plugins/tests/components/collections/sites/open/job_test/all_biz_execute_job_plan/test_v1_0.py index ec85a8e222..51ab94ccbd 100644 --- a/pipeline_plugins/tests/components/collections/sites/open/job_test/all_biz_execute_job_plan/test_v1_0.py +++ b/pipeline_plugins/tests/components/collections/sites/open/job_test/all_biz_execute_job_plan/test_v1_0.py @@ -13,19 +13,21 @@ import ujson as json from django.test import TestCase from mock import MagicMock - from pipeline.component_framework.test import ( - ComponentTestMixin, - ComponentTestCase, + Call, CallAssertion, + ComponentTestCase, + ComponentTestMixin, ExecuteAssertion, - ScheduleAssertion, - Call, Patcher, + ScheduleAssertion, ) + +from gcloud.utils import crypto from pipeline_plugins.components.collections.sites.open.job.all_biz_execute_job_plan.v1_0 import ( AllBizJobExecuteJobPlanComponent, ) +from pipeline_plugins.components.query.sites.open.job import JOBV3_VAR_CATEGORY_PASSWORD class AllBizJobExecuteJobPlanComponentTest(TestCase, ComponentTestMixin): @@ -228,7 +230,15 @@ def __init__( "data": { "job_instance_id": 10000, "step_instance_var_list": [ - {"step_instance_id": 20000000577, "global_var_list": [{"type": 1, "name": "name", "value": "test"}]}, + { + "step_instance_id": 20000000577, + "global_var_list": [ + # 已重新编辑,需要传递给 Job + {"type": JOBV3_VAR_CATEGORY_PASSWORD, "name": "password_1", "value": "test"}, + # Job 侧脱敏,无需传递 + {"type": JOBV3_VAR_CATEGORY_PASSWORD, "name": "password", "value": "******"}, + ], + }, {"step_instance_id": 20000000578, "global_var_list": [{"type": 1, "name": "name", "value": "test"}]}, ], }, @@ -700,6 +710,12 @@ def __init__( "is_tagged_ip": True, "job_global_var": [ {"id": 1000030, "type": 1, "name": "name", "value": "test", "description": ""}, + { + "id": 1000032, + "type": JOBV3_VAR_CATEGORY_PASSWORD, + "name": "password", + "value": {"tag": "variable", "value": crypto.encrypt("123")}, + }, {"id": 1000031, "type": 3, "name": "ip", "value": "0:192.168.20.218", "description": ""}, ], } @@ -741,6 +757,7 @@ def __init__( "job_plan_id": 1000010, "global_var_list": [ {"id": 1000030, "value": "test"}, + {"id": 1000032, "value": "123"}, {"id": 1000031, "server": {"ip_list": [{"ip": "192.168.20.218", "bk_cloud_id": 0}]}}, ], "callback_url": "callback_url", diff --git a/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v2_create_task.py b/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v2_create_task.py index f271bbffc8..4f56efcc31 100644 --- a/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v2_create_task.py +++ b/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v2_create_task.py @@ -13,16 +13,16 @@ from django.test import TestCase from mock import MagicMock - from pipeline.component_framework.test import ( Call, CallAssertion, ComponentTestCase, ComponentTestMixin, ExecuteAssertion, - ScheduleAssertion, Patcher, + ScheduleAssertion, ) + from pipeline_plugins.components.collections.sites.open.nodeman.create_task.v2_0 import NodemanCreateTaskComponent @@ -71,7 +71,9 @@ def __init__( GET_HOST_ID_BY_INNER_IP_BASE = ( "pipeline_plugins.components.collections.sites.open.nodeman.create_task.v2_0.get_host_id_by_inner_ip" ) -ENCRYPT_AUTH_KEY = "pipeline_plugins.components.collections.sites.open.nodeman.create_task.v2_0.encrypt_auth_key" +ENCRYPT_AUTH_KEY = ( + "pipeline_plugins.components.collections.sites.open.nodeman." "base.NodeManBaseService.parse2nodeman_ciphertext" +) # mock clients CASE_FAIL_CLIENT = MockClient( @@ -743,7 +745,6 @@ def __init__( inputs={ "bk_biz_id": "1", "nodeman_op_target": {"nodeman_bk_cloud_id": "1", "nodeman_node_type": "AGENT"}, - "nodeman_ticket": {"nodeman_tjj_ticket": "xxxxx"}, "nodeman_op_info": { "nodeman_ap_id": "1", "nodeman_op_type": "INSTALL", @@ -798,7 +799,6 @@ def __init__( "data_ip": "1.1.1.1", } ], - "tcoa_ticket": "xxxxx", } ) ], diff --git a/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v3_create_task.py b/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v3_create_task.py index 5836b6ad1f..bc249cb008 100644 --- a/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v3_create_task.py +++ b/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v3_create_task.py @@ -75,7 +75,9 @@ def __init__( ) GET_HOST_ID_BY_INNER_IP = "pipeline_plugins.components.collections.sites.open.nodeman.base.get_host_id_by_inner_ip" -ENCRYPT_AUTH_KEY = "pipeline_plugins.components.collections.sites.open.nodeman.create_task.v3_0.encrypt_auth_key" +ENCRYPT_AUTH_KEY = ( + "pipeline_plugins.components.collections.sites.open.nodeman." "base.NodeManBaseService.parse2nodeman_ciphertext" +) # mock clients CASE_FAIL_CLIENT = MockClient( diff --git a/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v4_create_task.py b/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v4_create_task.py index 8ff67501a0..71e49df389 100644 --- a/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v4_create_task.py +++ b/pipeline_plugins/tests/components/collections/sites/open/nodeman_test/create_task_test/test_nodeman_v4_create_task.py @@ -71,7 +71,9 @@ def __init__( HANDLE_API_ERROR = "pipeline_plugins.components.collections.sites.open.nodeman.base.handle_api_error" GET_BUSINESS_HOST = "pipeline_plugins.components.collections.sites.open.nodeman.create_task.v4_0.get_business_host" GET_HOST_ID_BY_INNER_IP = "pipeline_plugins.components.collections.sites.open.nodeman.base.get_host_id_by_inner_ip" -ENCRYPT_AUTH_KEY = "pipeline_plugins.components.collections.sites.open.nodeman.create_task.v4_0.encrypt_auth_key" +ENCRYPT_AUTH_KEY = ( + "pipeline_plugins.components.collections.sites.open.nodeman." "base.NodeManBaseService.parse2nodeman_ciphertext" +) # mock clients CASE_FAIL_CLIENT = MockClient( diff --git a/pipeline_plugins/tests/components/collections/sites/open/wechat_work_test/wechat_work_send_message/test_wechat_work_send_message_v1_0.py b/pipeline_plugins/tests/components/collections/sites/open/wechat_work_test/wechat_work_send_message/test_wechat_work_send_message_v1_0.py index d77bd82e3e..839e50f3a8 100644 --- a/pipeline_plugins/tests/components/collections/sites/open/wechat_work_test/wechat_work_send_message/test_wechat_work_send_message_v1_0.py +++ b/pipeline_plugins/tests/components/collections/sites/open/wechat_work_test/wechat_work_send_message/test_wechat_work_send_message_v1_0.py @@ -13,7 +13,6 @@ from django.test import TestCase from mock import MagicMock - from pipeline.component_framework.test import ( Call, CallAssertion, @@ -22,9 +21,11 @@ ExecuteAssertion, Patcher, ) -from pipeline_plugins.components.collections.sites.open.wechat_work.wechat_work_send_message.v1_0 import ( + +from gcloud.utils import crypto +from pipeline_plugins.components.collections.sites.open.wechat_work.wechat_work_send_message.v1_0 import ( # noqa WechatWorkSendMessageComponent, -) # noqa +) class WechatWorkSendMessageComponentTest(TestCase, ComponentTestMixin): @@ -89,7 +90,7 @@ def component_cls(self): "msgtype": "text", }, parent_data={}, - execute_assertion=ExecuteAssertion(success=False, outputs={"ex_data": "无效的会话 ID: @all"}), + execute_assertion=ExecuteAssertion(success=False, outputs={"ex_data": "第1行的会话 ID 格式不正确(长度需为 32)"}), schedule_assertion=None, patchers=[Patcher(target=ENVIRONMENT_VAIRABLES_GET, return_value="test_url")], ) @@ -172,3 +173,40 @@ def component_cls(self): Patcher(target=REQUESTS_POST, side_effect=SUCCESS_REQUEST_POST), ], ) + + +ENCRYPT_SUCCESS_CASE = ComponentTestCase( + name="success case", + inputs={ + "wechat_work_chat_id": { + "tag": "variable", + "value": crypto.encrypt("11111111111111111111111111111111\n22222222222222222222222222222222"), + }, + "message_content": "haha", + "wechat_work_mentioned_members": "m1,m2", + "msgtype": "text", + }, + parent_data={}, + execute_assertion=ExecuteAssertion(success=True, outputs={}), + schedule_assertion=None, + execute_call_assertion=[ + CallAssertion( + func=SUCCESS_REQUEST_POST, + calls=[ + Call( + url="test_url", + json={ + "chatid": "11111111111111111111111111111111|22222222222222222222222222222222", # noqa + "msgtype": "text", + "text": {"content": "haha", "mentioned_list": ["m1", "m2"]}, + }, + timeout=5, + ) + ], + ) + ], + patchers=[ + Patcher(target=ENVIRONMENT_VAIRABLES_GET, return_value="test_url"), + Patcher(target=REQUESTS_POST, side_effect=SUCCESS_REQUEST_POST), + ], +) diff --git a/pipeline_plugins/tests/utils/test_utils.py b/pipeline_plugins/tests/utils/test_utils.py index c47611a779..50a1549b8a 100644 --- a/pipeline_plugins/tests/utils/test_utils.py +++ b/pipeline_plugins/tests/utils/test_utils.py @@ -13,7 +13,7 @@ from django.test import TestCase -from pipeline_plugins.components.utils import chunk_table_data +from pipeline_plugins.components.utils import chunk_table_data, parse_passwd_value class UtilsTestCase(TestCase): @@ -65,3 +65,15 @@ def test_chunk_table_data(self): self.assertEqual(has_two_list_expect_column, has_two_list_actual_column["data"]) self.assertEqual([], failed_actual_column["data"]) self.assertFalse(failed_actual_column["result"]) + + def test_parse_passwd_value__nest(self): + value = "123" + passwd_value = {"tag": "value", "value": {"tag": "value", "value": value}} + self.assertEqual(parse_passwd_value(passwd_value), value) + + def test_parse_passwd_value__str(self): + value = "123" + self.assertEqual(parse_passwd_value(value), value) + + def test_parse_passwd_value__invalid_type(self): + self.assertEqual(parse_passwd_value([]), "") diff --git a/pipeline_plugins/variables/collections/sites/open/cc.py b/pipeline_plugins/variables/collections/sites/open/cc.py index 1cb182c205..b7c5f8ee09 100644 --- a/pipeline_plugins/variables/collections/sites/open/cc.py +++ b/pipeline_plugins/variables/collections/sites/open/cc.py @@ -225,7 +225,9 @@ def _self_explain(cls, **kwargs) -> List[FieldExplain]: FieldExplain(key="${KEY._module}", type=Type.LIST, description="集群下的模块信息列表,元素类型为字典,键为模块名,值为模块下的主机列"), FieldExplain(key="${KEY.flat__ip_list}", type=Type.STRING, description="本次操作创建的所有集群下的主机(去重后),用 ',' 连接"), FieldExplain( - key="${KEY.flat__verbose_ip_list}", type=Type.STRING, description="返回的是本次操作创建的所有集群下的主机(未去重),用 ',' 连接", + key="${KEY.flat__verbose_ip_list}", + type=Type.STRING, + description="返回的是本次操作创建的所有集群下的主机(未去重),用 ',' 连接", ), FieldExplain( key="${KEY.flat__verbose_ip_module_list}", @@ -304,7 +306,13 @@ def _handle_value_with_ipv4(username, bk_biz_id, bk_supplier_account, host_field if not ip_list: return [] - hosts_list = get_business_host(username, bk_biz_id, bk_supplier_account, host_fields, ip_list,) + hosts_list = get_business_host( + username, + bk_biz_id, + bk_supplier_account, + host_fields, + ip_list, + ) return hosts_list @staticmethod @@ -408,15 +416,7 @@ def get_value(self): origin_ip_list = get_plat_ip_by_regex(origin_ips) filter_data = {**self.value, **self.pipeline_data} - if not settings.ENABLE_IPV6: - # 进行gse agent状态过滤 - gse_agent_status_filter = GseAgentStatusIpFilter(origin_ip_list, filter_data) - match_result_ip = gse_agent_status_filter.get_match_ip() - if not ip_cloud: - return ip_separator.join(["{}".format(host["ip"]) for host in match_result_ip]) - - return ip_separator.join(["{}:{}".format(host["bk_cloud_id"], host["ip"]) for host in match_result_ip]) - else: + if settings.ENABLE_IPV6: gse_agent_status_ipv6_filter = GseAgentStatusIpV6Filter(origin_ips, filter_data) match_result_ip = gse_agent_status_ipv6_filter.get_match_ip() if not ip_cloud: @@ -430,3 +430,11 @@ def get_value(self): else: result.append("{}:{}".format(host["bk_cloud_id"], host["ip"])) return ip_separator.join(result) + else: + # 进行gse agent状态过滤 + gse_agent_status_filter = GseAgentStatusIpFilter(origin_ip_list, filter_data) + match_result_ip = gse_agent_status_filter.get_match_ip() + if not ip_cloud: + return ip_separator.join(["{}".format(host["ip"]) for host in match_result_ip]) + + return ip_separator.join(["{}:{}".format(host["bk_cloud_id"], host["ip"]) for host in match_result_ip]) diff --git a/pipeline_plugins/variables/collections/sites/open/ip_filter_base.py b/pipeline_plugins/variables/collections/sites/open/ip_filter_base.py index 53c4f54910..833d1c9c08 100644 --- a/pipeline_plugins/variables/collections/sites/open/ip_filter_base.py +++ b/pipeline_plugins/variables/collections/sites/open/ip_filter_base.py @@ -13,16 +13,18 @@ import logging from abc import ABCMeta, abstractmethod +from django.conf import settings from django.utils.translation import ugettext_lazy as _ -from gcloud.constants import GseAgentStatus from gcloud.conf import settings as gcloud_settings +from gcloud.constants import GseAgentStatus from gcloud.core.models import Project from gcloud.exceptions import ApiRequestError +from gcloud.utils import cmdb from gcloud.utils.handlers import handle_api_error -from gcloud.utils.ip import extract_ip_from_ip_str, get_ip_by_regex_type, IpRegexType -from pipeline_plugins.base.utils.inject import supplier_id_for_project, supplier_account_for_business -from pipeline_plugins.cmdb_ip_picker.utils import get_ges_agent_status_ipv6 +from gcloud.utils.ip import IpRegexType, extract_ip_from_ip_str, get_ip_by_regex_type +from pipeline_plugins.base.utils.inject import supplier_account_for_business, supplier_id_for_project +from pipeline_plugins.cmdb_ip_picker.utils import get_gse_agent_status_ipv6 from pipeline_plugins.components.collections.sites.open.cc.base import cc_get_host_by_innerip_with_ipv6 logger = logging.getLogger("root") @@ -40,18 +42,57 @@ def get_match_ip(self): class GseAgentStatusIpFilter(IpFilterBase): - def get_match_ip(self): + def match_ges_v2(self, gse_agent_status, username, bk_biz_id, bk_supplier_id, origin_ip_list): - origin_ip_list = self.origin_ip_list - gse_agent_status = self.data.get("gse_agent_status", "") - username = self.data["executor"] - project_id = self.data["project_id"] - project = Project.objects.get(id=project_id) - bk_biz_id = project.bk_biz_id if project.from_cmdb else "" - bk_supplier_id = supplier_id_for_project(project_id) - if not origin_ip_list: - return [] + fields = ["bk_host_id", "bk_cloud_id", "bk_host_innerip", "bk_agent_id"] + # 生成一个host列表 + origin_hosts = { + "{}:{}".format(origin_host["bk_cloud_id"], origin_host["ip"]): origin_host for origin_host in origin_ip_list + } + + ip_list = [host["ip"] for host in origin_ip_list] + # 先去查出来所有host的gse_agent_id + hosts = cmdb.get_business_host(username, bk_biz_id, bk_supplier_id, host_fields=fields, ip_list=ip_list) + + # 构造一个{bk_agent_id: bk_cloud_id:ip}的字典 + remote_hosts = {} + for host in hosts: + remote_host_value = "{}:{}".format(host["bk_cloud_id"], host["bk_host_innerip"]) + if remote_host_value in origin_hosts.keys(): + bk_agent_id = host.get("bk_agent_id") + if not bk_agent_id: + # 没有agent_id 使用 云区域+ip 组成 agent_id + bk_agent_id = remote_host_value + remote_hosts[bk_agent_id] = remote_host_value + + # 去查询agent状态 + agent_map = get_gse_agent_status_ipv6(bk_agent_id_list=list(remote_hosts.keys())) + + agent_online_ip_list = [] # 在线的ip的列表 + agent_offline_ip_list = [] # 不在线的ip的列表 + match_ip = [] # 过滤失败将不返回任何i + for bk_agent_id, agent_code in agent_map.items(): + origin_host_key = remote_hosts.get(bk_agent_id) + if not origin_host_key: + continue + origin_host = origin_hosts.get(origin_host_key) + if not origin_host: + continue + + if agent_code == GseAgentStatus.ONlINE.value: + agent_online_ip_list.append(origin_host) + if agent_code == GseAgentStatus.OFFLINE.value: + agent_offline_ip_list.append(origin_host) + + if gse_agent_status == GseAgentStatus.ONlINE.value: + match_ip = agent_online_ip_list + if gse_agent_status == GseAgentStatus.OFFLINE.value: + match_ip = agent_offline_ip_list + + return match_ip + + def match_gse_v1(self, gse_agent_status, username, bk_biz_id, bk_supplier_id, origin_ip_list): match_ip = origin_ip_list if gse_agent_status in [GseAgentStatus.ONlINE.value, GseAgentStatus.OFFLINE.value]: client = get_client_by_user(username) @@ -81,6 +122,23 @@ def get_match_ip(self): return match_ip + def get_match_ip(self): + + origin_ip_list = self.origin_ip_list + gse_agent_status = self.data.get("gse_agent_status", "") + username = self.data["executor"] + project_id = self.data["project_id"] + project = Project.objects.get(id=project_id) + bk_biz_id = project.bk_biz_id if project.from_cmdb else "" + bk_supplier_id = supplier_id_for_project(project_id) + if not origin_ip_list: + return [] + + if settings.ENABLE_GSE_V2: + return self.match_ges_v2(gse_agent_status, username, bk_biz_id, bk_supplier_id, origin_ip_list) + else: + return self.match_gse_v1(gse_agent_status, username, bk_biz_id, bk_supplier_id, origin_ip_list) + class GseAgentStatusIpV6Filter: def __init__(self, ip_str, data): @@ -121,7 +179,7 @@ def get_match_ip(self): bk_agent_id_list.append(bk_agent_id) try: - agent_id_status_map = get_ges_agent_status_ipv6(bk_agent_id_list) + agent_id_status_map = get_gse_agent_status_ipv6(bk_agent_id_list) except Exception as e: raise ApiRequestError(f"ERROR:{e}") @@ -143,7 +201,7 @@ def get_match_ip(self): agent_online_ip_list.append(match_result) # agent 状态为 0 或者 未知 则认为 该主机 不在线 if agent_id_status_map.get(bk_agent_id, 0) in [0, -1]: - agent_online_ip_list.append(match_result) + agent_offline_ip_list.append(match_result) gse_agent_status = self.data.get("gse_agent_status", "") if gse_agent_status == GseAgentStatus.ONlINE.value: match_host = agent_online_ip_list diff --git a/pipeline_plugins/variables/static/variables/password.js b/pipeline_plugins/variables/static/variables/password.js index 6fcb8535c4..7fb8f0b3b3 100644 --- a/pipeline_plugins/variables/static/variables/password.js +++ b/pipeline_plugins/variables/static/variables/password.js @@ -17,6 +17,8 @@ attrs: { name: gettext("密码"), hookable: true, + // TODO 如果是 false,应该是允许输密码? + canUseVar: false, validation: [ { type: "required" @@ -24,5 +26,5 @@ ] } }, - ] + ]; })(); diff --git a/requirements.txt b/requirements.txt index 48efaa1641..4a9c3f02cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -61,7 +61,7 @@ importlib-metadata==3.7.3 protobuf==3.19.4 # blueapps -blueapps[opentelemetry]==4.7.0 +blueapps[opentelemetry,bkcrypto]==4.8.0 whitenoise==5.2.0 raven==6.5.0 python-json-logger==2.0.1 diff --git a/version_logs_md/V3.29.0_2023-07-04.md b/version_logs_md/V3.29.0_2023-07-04.md index 82a4d176c7..2f75a2c3d5 100644 --- a/version_logs_md/V3.29.0_2023-07-04.md +++ b/version_logs_md/V3.29.0_2023-07-04.md @@ -8,7 +8,7 @@ - [ 优化 ] 移除「节点管理(Nodeman)-插件操作」里的主机 ID 选项 -- [ 优化 ] 新建职能化任务添加复制认领链接 +- [ 优化 ] 创建职能化任务时增加通知认领的引导交互 ### 修复 @@ -26,10 +26,10 @@ - [ 修复 ] 修复使用「蓝鲸监控告警屏蔽(按范围」 插件屏蔽业务时,还需再选业务的问题 -- [ 修复 ] 修复<作业平台(JOB)-执行作业>插件,拉取执行方案时,变量值多了双引号问题 +- [ 修复 ] 修复「作业平台(JOB)-执行作业」插件,拉取执行方案时,变量值多了双引号问题 - [ 修复 ] 修复任务列表还在加载时,切换 TAB 返回空数据的问题 - [ 修复 ] 修复重新执行时,下拉框选项无法重用和粘贴文本自动选中的问题 -- [ 修复 ] 修复第三方插件日志获取异常的问题(Note: 依赖 PaaS 版本 v1.1.0-beta.38+) +- [ 修复 ] 修复第三方插件日志获取异常问题(依赖 Paas v1.1.0-beta.38 版本) diff --git a/version_logs_md/V3.30.0_2023-08-30.md b/version_logs_md/V3.30.0_2023-08-30.md new file mode 100644 index 0000000000..d47990eff2 --- /dev/null +++ b/version_logs_md/V3.30.0_2023-08-30.md @@ -0,0 +1,3 @@ +# V3.30.0 版本更新说明 + +- [ 新增 ] 提供国密支持 diff --git a/version_logs_md_en/V3.29.0_2023-07-04.md b/version_logs_md_en/V3.29.0_2023-07-04.md new file mode 100644 index 0000000000..95f33c2431 --- /dev/null +++ b/version_logs_md_en/V3.29.0_2023-07-04.md @@ -0,0 +1,35 @@ +# V3.29.0 Release Note + +### Improved + +- [Improved] Optimized the layout of the task parameter filling page + +- [Improved] Optimized the interaction of the node-group + +- [Improved] Removed the host ID field in "NodeMan-Plugin Operation" plugin + +- [Improved] Added guide interaction for taking task when assigning task to helper-team + +### Fixed + +- [Fixed] Fixed the problem of abnormal node size and operation panel position when zooming the canvas + +- [Fixed] Fixed the authentication issue of cloned flow without new creation permission when viewing flow + +- [Fixed] Fixed the problem of missing global variables during YAML import + +- [Fixed] Fixed the problem of some nodes being covered after clicking "View Subflow that need to be updated" + +- [Fixed] Fixed the problem of remote data source for select variables not supporting URL parameters + +- [Fixed] Fixed the problem of can't terminate task when subflow is paused + +- [Fixed] Fixed the problem of duplicate selection business when using "BKMonitor-Alarm Shield of BlueKing Monitor (Based on Range)" plugin to shield business + +- [Fixed] Fixed the problem of extra double quotes in variable values when using "JOB-Execution JOB" plugin + +- [Fixed] Fixed the problem of returning empty data when switching TAB while task list is still loading + +- [Fixed] Fixed the problem of unable to reuse drop-down box options and automatically selecting pasted text when using redo task + +- [Fixed] Fixed the issue of abnormal third-party plugin log retrieval (dependent on Paas v1.1.0-beta.38 version)