diff --git a/apps/backend/components/collections/agent_new/install.py b/apps/backend/components/collections/agent_new/install.py index b5076bf8d..55093fc92 100644 --- a/apps/backend/components/collections/agent_new/install.py +++ b/apps/backend/components/collections/agent_new/install.py @@ -13,6 +13,7 @@ import json import os import random +import re import socket import time import typing @@ -28,6 +29,7 @@ from apps.backend.agent.tools import InstallationTools from apps.backend.api.constants import POLLING_INTERVAL from apps.backend.constants import ( + POWERSHELL_SERVICE_CHECK_SSHD, REDIS_AGENT_CONF_KEY_TPL, REDIS_INSTALL_CALLBACK_KEY_TPL, SSH_RUN_TIMEOUT, @@ -674,7 +676,13 @@ async def execute_shell_solution_async( execution_solution = installation_tool.type__execution_solution_map[ constants.CommonExecutionSolutionType.SHELL.value ] + command_converter: Dict = {} + async with conns.AsyncsshConn(**install_sub_inst_obj.conns_init_params) as conn: + if install_sub_inst_obj.host.os_type == constants.OsType.WINDOWS: + sshd_info = await conn.run(POWERSHELL_SERVICE_CHECK_SSHD, check=True, timeout=SSH_RUN_TIMEOUT) + sshd_info = sshd_info.stdout.lower() + self.build_shell_to_batch_command_converter(execution_solution.steps, command_converter, sshd_info) for execution_solution_step in execution_solution.steps: if execution_solution_step.type == constants.CommonExecutionSolutionStepType.DEPENDENCIES.value: @@ -699,12 +707,59 @@ async def execute_shell_solution_async( elif execution_solution_step.type == constants.CommonExecutionSolutionStepType.COMMANDS.value: for content in execution_solution_step.contents: - cmd = content.text + cmd = command_converter.get(content.text, content.text) + if not cmd: + continue await log_info(sub_inst_ids=sub_inst_id, log_content=_("执行命令: {cmd}").format(cmd=cmd)) await conn.run(command=cmd, check=True, timeout=SSH_RUN_TIMEOUT) return sub_inst_id + @classmethod + def convert_shell_to_powershell(cls, shell_cmd): + # Convert mkdir -p xxx to if not exist xxx mkdir xxx + shell_cmd = re.sub( + r"mkdir -p\s+(\S+)", + r"powershell -c 'if (-Not (Test-Path -Path \1)) { New-Item -ItemType Directory -Path \1 }'", + shell_cmd, + ) + + # Convert chmod +x xxx to '' + shell_cmd = re.sub(r"chmod\s+\+x\s+\S+", r"", shell_cmd) + + # Convert curl to Invoke-WebRequest + # shell_cmd = re.sub( + # r"curl\s+(http[s]?:\/\/[^\s]+)\s+-o\s+(\/?[^\s]+)\s+--connect-timeout\s+(\d+)\s+-sSfg", + # r"powershell -c 'Invoke-WebRequest -Uri \1 -OutFile \2 -TimeoutSec \3 -UseBasicParsing'", + # shell_cmd, + # ) + shell_cmd = re.sub(r"(curl\s+\S+\s+-o\s+\S+\s+--connect-timeout\s+\d+\s+-sSfg)", r'cmd /c "\1"', shell_cmd) + + # Convert nohup xxx &> ... & to xxx (ignore nohup, output redirection and background execution) + shell_cmd = re.sub( + r"nohup\s+([^&>]+)(\s*&>\s*.*?&)?", + r"powershell -c 'Invoke-Command -Session (New-PSSession) -ScriptBlock { \1 } -AsJob'", + shell_cmd, + ) + + # Remove '&>' and everything after it + shell_cmd = re.sub(r"\s*&>.*", "", shell_cmd) + + # Convert \\ to \ + shell_cmd = shell_cmd.replace("\\\\", "\\") + + return shell_cmd.strip() + + def build_shell_to_batch_command_converter(self, steps, command_converter, sshd_info): + if "cygwin" in sshd_info: + return + + for execution_solution_step in steps: + if execution_solution_step.type == constants.CommonExecutionSolutionStepType.COMMANDS.value: + for content in execution_solution_step.contents: + cmd = content.text + command_converter[cmd] = self.convert_shell_to_powershell(cmd) + def handle_report_data(self, host: models.Host, sub_inst_id: int, success_callback_step: str) -> Dict: """处理上报数据""" name = REDIS_INSTALL_CALLBACK_KEY_TPL.format(sub_inst_id=sub_inst_id) diff --git a/apps/backend/constants.py b/apps/backend/constants.py index e923d4b76..e024154c6 100644 --- a/apps/backend/constants.py +++ b/apps/backend/constants.py @@ -164,3 +164,5 @@ def needs_batch_request(self) -> bool: DEFAULT_ALIVE_TIME = 30 DEFAULT_CLEAN_RECORD_LIMIT = 5000 + +POWERSHELL_SERVICE_CHECK_SSHD = "powershell -c Get-Service -Name sshd" diff --git a/apps/core/remote/conns/base.py b/apps/core/remote/conns/base.py index 94ab934dc..00b988e75 100644 --- a/apps/core/remote/conns/base.py +++ b/apps/core/remote/conns/base.py @@ -33,7 +33,10 @@ def __init__(self, command: BytesOrStr, exit_status: int, stdout: BytesOrStr, st @staticmethod def bytes2str(val: BytesOrStr) -> str: if isinstance(val, bytes): - return val.decode(encoding="utf-8") + try: + return val.decode(encoding="utf-8") + except UnicodeDecodeError: + return val.decode(encoding="gbk") return val def __str__(self): diff --git a/script_tools/agent_tools/agent2/setup_agent.bat b/script_tools/agent_tools/agent2/setup_agent.bat index 86954cfb5..f6a89f7b7 100644 --- a/script_tools/agent_tools/agent2/setup_agent.bat +++ b/script_tools/agent_tools/agent2/setup_agent.bat @@ -549,9 +549,9 @@ goto :EOF goto :EOF :get_config - call :print INFO get_config - "request %NODE_TYPE% config files" + call :print INFO get_config - "request agent config files" call :multi_report_step_status - set PARAM="{\"bk_cloud_id\":%CLOUD_ID%,\"filename\":\"gse_agent.conf\",\"node_type\":\"%NODE_TYPE%\",\"inner_ip\":\"%LAN_ETH_IP%\",\"token\":\"%TOKEN%\"}" + set PARAM="{\"bk_cloud_id\":%CLOUD_ID%,\"filename\":\"gse_agent.conf\",\"node_type\":\"agent\",\"inner_ip\":\"%LAN_ETH_IP%\",\"token\":\"%TOKEN%\"}" echo call :print INFO get_config - "request config files with: %PARAM%" for %%p in (gse_agent.conf) do ( if "%HTTP_PROXY%" == "" ( diff --git a/script_tools/setup_pagent.py b/script_tools/setup_pagent.py index f2dc73150..98839cab1 100644 --- a/script_tools/setup_pagent.py +++ b/script_tools/setup_pagent.py @@ -199,6 +199,8 @@ def logging( JOB_PRIVATE_KEY_RE = re.compile(r"^(-{5}BEGIN .*? PRIVATE KEY-{5})(.*?)(-{5}END .*? PRIVATE KEY-{5}.?)$") +POWERSHELL_SERVICE_CHECK_SSHD = "powershell -c Get-Service -Name sshd" + def is_ip(ip: str, _version: Optional[int] = None) -> bool: """ @@ -312,6 +314,41 @@ def execute_batch_solution( print(res) +def convert_shell_to_powershell(shell_cmd): + # Convert mkdir -p xxx to if not exist xxx mkdir xxx + shell_cmd = re.sub( + r"mkdir -p\s+(\S+)", + r"powershell -c 'if (-Not (Test-Path -Path \1)) { New-Item -ItemType Directory -Path \1 }'", + shell_cmd, + ) + + # Convert chmod +x xxx to '' + shell_cmd = re.sub(r"chmod\s+\+x\s+\S+", r"", shell_cmd) + + # Convert curl to Invoke-WebRequest + # shell_cmd = re.sub( + # r"curl\s+(http[s]?:\/\/[^\s]+)\s+-o\s+(\/?[^\s]+)\s+--connect-timeout\s+(\d+)\s+-sSfg", + # r"powershell -c 'Invoke-WebRequest -Uri \1 -OutFile \2 -TimeoutSec \3 -UseBasicParsing'", + # shell_cmd, + # ) + shell_cmd = re.sub(r"(curl\s+\S+\s+-o\s+\S+\s+--connect-timeout\s+\d+\s+-sSfg)", r'cmd /c "\1"', shell_cmd) + + # Convert nohup xxx &> ... & to xxx (ignore nohup, output redirection and background execution) + shell_cmd = re.sub( + r"nohup\s+([^&>]+)(\s*&>\s*.*?&)?", + r"powershell -c 'Invoke-Command -Session (New-PSSession) -ScriptBlock { \1 } -AsJob'", + shell_cmd, + ) + + # Remove '&>' and everything after it + shell_cmd = re.sub(r"\s*&>.*", "", shell_cmd) + + # Convert \\ to \ + shell_cmd = shell_cmd.replace("\\\\", "\\") + + return shell_cmd.strip() + + def execute_shell_solution( login_ip: str, account: str, @@ -333,12 +370,25 @@ def execute_shell_solution( client_key_strings=client_key_strings, connect_timeout=15, ) as conn: + command_converter = {} + if os_type == "windows": + run_output: RunOutput = conn.run(POWERSHELL_SERVICE_CHECK_SSHD, check=True, timeout=30) + if run_output.exit_status == 0 and "cygwin" not in run_output.stdout.lower(): + for step in execution_solution["steps"]: + if step["type"] != "commands": + continue + for content in step["contents"]: + cmd: str = content["text"] + command_converter[cmd] = convert_shell_to_powershell(cmd) + for step in execution_solution["steps"]: # 暂不支持 dependencies 等其他步骤类型 if step["type"] != "commands": continue for content in step["contents"]: - cmd: str = content["text"] + cmd: str = command_converter.get(content["text"], content["text"]) + if not cmd: + continue # 根据用户名判断是否采用sudo if account not in ["root", "Administrator", "administrator"] and not cmd.startswith("sudo"): @@ -542,7 +592,10 @@ def __init__(self, command: BytesOrStr, exit_status: int, stdout: BytesOrStr, st @staticmethod def bytes2str(val: BytesOrStr) -> str: if isinstance(val, bytes): - return val.decode(encoding="utf-8") + try: + return val.decode(encoding="utf-8") + except UnicodeDecodeError: + return val.decode(encoding="gbk") return val def __str__(self):