Skip to content

Commit

Permalink
feat: WINDOWS适配原生openssh (closed #2322)
Browse files Browse the repository at this point in the history
  • Loading branch information
ping15 authored and wyyalt committed Jul 25, 2024
1 parent 1e7232f commit 36d9d97
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 6 deletions.
57 changes: 56 additions & 1 deletion apps/backend/components/collections/agent_new/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import json
import os
import random
import re
import socket
import time
import typing
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 4 additions & 1 deletion apps/core/remote/conns/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions script_tools/agent_tools/agent2/setup_agent.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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%" == "" (
Expand Down
57 changes: 55 additions & 2 deletions script_tools/setup_pagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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,
Expand All @@ -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"):
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 36d9d97

Please sign in to comment.