Skip to content

Commit

Permalink
feature: Windows Agent安装支持用administrator用户注册服务(close TencentBlueKing#277
Browse files Browse the repository at this point in the history
)
  • Loading branch information
zhangzhw8 committed Dec 7, 2021
1 parent 905d097 commit c3bc0cc
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 132 deletions.
56 changes: 34 additions & 22 deletions apps/backend/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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 os
import re
import time
from pathlib import Path
Expand All @@ -22,14 +22,15 @@
from apps.node_man import constants, models
from apps.node_man.models import aes_cipher
from apps.utils.basic import suffix_slash
from apps.utils.encrypt import rsa


class InstallationTools:
def __init__(
self,
script_file_name: str,
dest_dir: str,
win_commands: str,
win_commands: List[str],
upstream_nodes: List[str],
jump_server: models.Host,
pre_commands: List[str],
Expand Down Expand Up @@ -139,7 +140,7 @@ def gen_commands(host: models.Host, pipeline_id: str, is_uninstall: bool) -> Ins
proxy 云区域所使用的代理, pre_commands 安装前命令, run_cmd 安装命令
"""
proxies = []
win_commands = []
encrypted_password = ""
(
jump_server,
bt_file_servers,
Expand Down Expand Up @@ -173,6 +174,15 @@ def gen_commands(host: models.Host, pipeline_id: str, is_uninstall: bool) -> Ins
f'-k "{task_servers}"',
]

# 系统开启使用密码注册windows服务时,需额外传入用户名和加密密码参数,用于注册windows服务,详见setup_agent.bat脚本
need_encrypted_password = settings.REGISTER_WIN_SERVICE_WITH_PASS and host.os_type == constants.OsType.WINDOWS
if need_encrypted_password:
# 系统开启使用密码注册windows服务时,需额外传入 -U -P参数,用于注册windows服务,详见setup_agent.bat脚本
encrypted_password = rsa.RSAUtil(
public_extern_key_file=os.path.join(settings.BK_SCRIPTS_PATH, "gse_public_key"),
padding=rsa.CipherPadding.PKCS1_OAEP.value,
).encrypt(host.identity.password)

check_run_commands(run_cmd_params)
script_file_name = choose_script_file(host)

Expand Down Expand Up @@ -208,14 +218,10 @@ def gen_commands(host: models.Host, pipeline_id: str, is_uninstall: bool) -> Ins
f"-HPP '{settings.BK_NODEMAN_NGINX_PROXY_PASS_PORT}'",
f"-HSN '{constants.SCRIPT_FILE_NAME_MAP[host.os_type]}'",
f"-HS '{host_shell}'",
]
)

run_cmd_params.extend(
[
f"-p '{install_path}'",
f"-I {jump_server.inner_ip}",
f"-o {gen_nginx_download_url(jump_server.inner_ip)}",
f"-HEP '{encrypted_password}'" if need_encrypted_password else "",
"-R" if is_uninstall else "",
]
)
Expand All @@ -231,7 +237,7 @@ def gen_commands(host: models.Host, pipeline_id: str, is_uninstall: bool) -> Ins
if channel_proxy_address:
run_cmd_params.extend([f"-CPA '{channel_proxy_address}'"])

run_cmd = " ".join(run_cmd_params)
run_cmd = " ".join(list(filter(None, run_cmd_params)))

download_cmd = (
f"if [ ! -e {dest_dir}{script_file_name} ] || "
Expand All @@ -251,20 +257,17 @@ def gen_commands(host: models.Host, pipeline_id: str, is_uninstall: bool) -> Ins
"-R" if is_uninstall else "",
]
)

run_cmd = format_run_cmd_by_os_type(host.os_type, f"{dest_dir}{script_file_name} {' '.join(run_cmd_params)}")
if host.os_type == constants.OsType.WINDOWS:
# WINDOWS 下的 Agent 安装
win_remove_cmd = (
f"del /q /s /f {dest_dir}{script_file_name} "
f"{dest_dir}{constants.SetupScriptFileName.GSECTL_BAT.value}"
)
win_download_cmd = (
f"{dest_dir}curl.exe {host.ap.package_inner_url}/{script_file_name}"
f" -o {dest_dir}{script_file_name} -sSf"
if need_encrypted_password:
run_cmd_params.extend(
[
f"-U {host.identity.account}",
f'-P "{encrypted_password}"',
]
)

win_commands = [win_remove_cmd, win_download_cmd, run_cmd]
run_cmd = format_run_cmd_by_os_type(
host.os_type, f"{dest_dir}{script_file_name} {' '.join(list(filter(None, run_cmd_params)))}"
)
download_cmd = f"curl {package_url}/{script_file_name} -o {dest_dir}{script_file_name} --connect-timeout 5 -sSf"
chmod_cmd = f"chmod +x {dest_dir}{script_file_name}"
pre_commands = [
Expand All @@ -275,7 +278,16 @@ def gen_commands(host: models.Host, pipeline_id: str, is_uninstall: bool) -> Ins
pre_commands.insert(0, f"mkdir -p {dest_dir}")

return InstallationTools(
script_file_name, dest_dir, win_commands, upstream_nodes, jump_server, pre_commands, run_cmd
script_file_name,
dest_dir,
[
f"{dest_dir}curl.exe {host.ap.package_inner_url}/{script_file_name} -o {dest_dir}{script_file_name} -sSf",
run_cmd,
],
upstream_nodes,
jump_server,
pre_commands,
run_cmd,
)


Expand Down
51 changes: 47 additions & 4 deletions apps/backend/tests/components/collections/agent/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def test_gen_agent_command(self):
f" -r http://127.0.0.1/backend -l http://127.0.0.1/download"
f" -c {token}"
f' -O 48668 -E 58925 -A 58625 -V 58930 -B 10020 -S 60020 -Z 60030 -K 10030 -e "" -a "" -k ""'
f" -i 0 -I 127.0.0.1 -N SERVER -p /usr/local/gse -T /tmp/ &"
f" -i 0 -I 127.0.0.1 -N SERVER -p /usr/local/gse -T /tmp/ &"
)
self.assertEqual(installation_tool.run_cmd, run_cmd)

Expand Down Expand Up @@ -139,7 +139,7 @@ def test_gen_win_command(self):
f"C:\\tmp\\setup_agent.bat -s {utils.JOB_TASK_PIPELINE_ID}"
f" -r http://127.0.0.1/backend -l http://127.0.0.1/download -c {token}"
f' -O 48668 -E 58925 -A 58625 -V 58930 -B 10020 -S 60020 -Z 60030 -K 10030 -e "" -a "" -k ""'
f" -i 0 -I 127.0.0.1 -N SERVER -p c:\\gse -T C:\\tmp\\ "
f" -i 0 -I 127.0.0.1 -N SERVER -p c:\\gse -T C:\\tmp\\"
)
self.assertEqual(installation_tool.run_cmd, run_cmd)

Expand All @@ -152,6 +152,25 @@ def tearDown(self):
)


class InstallWindowsAgentWithEncryptedPasswordSuccessTest(InstallWindowsAgentSuccessTest):
def setUp(self):
super().setUp()
settings.REGISTER_WIN_SERVICE_WITH_PASS = True

def test_gen_win_command(self):
host = models.Host.objects.get(bk_host_id=utils.BK_HOST_ID)
installation_tool = gen_commands(host, utils.JOB_TASK_PIPELINE_ID, is_uninstall=False)
token = re.match(r"(.*) -c (.*?) -O", installation_tool.run_cmd).group(2)
run_cmd = (
f"C:\\tmp\\setup_agent.bat -s {utils.JOB_TASK_PIPELINE_ID}"
f" -r http://127.0.0.1/backend -l http://127.0.0.1/download -c {token}"
f' -O 48668 -E 58925 -A 58625 -V 58930 -B 10020 -S 60020 -Z 60030 -K 10030 -e "" -a "" -k ""'
f" -i 0 -I 127.0.0.1 -N SERVER -p c:\\gse -T C:\\tmp\\ -U root -P "
)
# RSA每次加密的结果都不一样,因此只要保证startswith即可
self.assertTrue(installation_tool.run_cmd.startswith(run_cmd))


class InstallWindowsPasswordAuthOverdueTest(TestCase, ComponentTestMixin):
EXECUTE_CMD_MOCK_PATH = "apps.backend.components.collections.agent.execute_cmd"
PUT_FILE_MACK_PATH = "apps.backend.components.collections.agent.put_file"
Expand Down Expand Up @@ -331,7 +350,7 @@ def test_gen_pagent_command(self):
f" -HOT linux -HDD '/tmp/'"
f" -HPP '17981' -HSN 'setup_agent.sh' -HS 'bash'"
f" -p '/usr/local/gse' -I 1.1.1.1"
f" -o http://1.1.1.1:{settings.BK_NODEMAN_NGINX_DOWNLOAD_PORT}/ "
f" -o http://1.1.1.1:{settings.BK_NODEMAN_NGINX_DOWNLOAD_PORT}/"
)
self.assertEqual(installation_tool.run_cmd, run_cmd)

Expand Down Expand Up @@ -532,7 +551,7 @@ def test_gen_install_channel_agent_command(self):
f" -HOT linux -HDD '/tmp/'"
f" -HPP '17981' -HSN 'setup_agent.sh' -HS 'bash'"
f" -p '/usr/local/gse' -I 1.1.1.1"
f" -o http://1.1.1.1:{settings.BK_NODEMAN_NGINX_DOWNLOAD_PORT}/ "
f" -o http://1.1.1.1:{settings.BK_NODEMAN_NGINX_DOWNLOAD_PORT}/"
f" -ADP 'True' -CPA 'http://127.0.0.1:17981'"
)
self.assertEqual(installation_tool.run_cmd, run_cmd)
Expand All @@ -545,3 +564,27 @@ def tearDown(self):
models.JobTask.objects.filter(bk_host_id=utils.BK_HOST_ID, current_step__endswith=DESCRIPTION).exists()
)
self.job_client_v2.stop()


class InstallWindowsPAgentWithEncryptedPasswordSuccessTest(InstallPAgentSuccessTest):
def setUp(self):
super().setUp()
settings.REGISTER_WIN_SERVICE_WITH_PASS = True
models.Host.objects.filter(bk_host_id=utils.BK_HOST_ID).update(os_type=constants.OsType.WINDOWS)

def test_gen_pagent_command(self):
host = models.Host.objects.get(bk_host_id=utils.BK_HOST_ID)
installation_tool = gen_commands(host, utils.JOB_TASK_PIPELINE_ID, is_uninstall=False)
token = re.match(r"(.*) -c (.*?) -O", installation_tool.run_cmd).group(2)
run_cmd = (
f"-s {utils.JOB_TASK_PIPELINE_ID} -r http://127.0.0.1/backend -l http://127.0.0.1/download"
f" -c {token}"
f" -O 48668 -E 58925 -A 58625 -V 58930 -B 10020 -S 60020 -Z 60030 -K 10030"
f' -e "1.1.1.1" -a "1.1.1.1" -k "1.1.1.1" -L /data/bkee/public/bknodeman/download'
f" -HLIP 127.0.0.1 -HIIP 127.0.0.1 -HA root -HP 22 -HI 'aes_str:::H4MFaqax' -HC 0 -HNT PAGENT"
f" -HOT windows -HDD 'C:\\tmp\\'"
f" -HPP '17981' -HSN 'setup_agent.bat' -HS 'bash'"
f" -p 'c:\\gse' -I 1.1.1.1"
f" -o http://1.1.1.1:{settings.BK_NODEMAN_NGINX_DOWNLOAD_PORT}/ -HEP"
)
self.assertTrue(installation_tool.run_cmd.startswith(run_cmd))
5 changes: 5 additions & 0 deletions apps/node_man/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,8 @@ def init_settings(self):
),
)
settings.HEAD_PLUGINS = obj.v_json

obj, created = GlobalSettings.objects.get_or_create(
key=GlobalSettings.KeyEnum.REGISTER_WIN_SERVICE_WITH_PASS.value, defaults=dict(v_json=False)
)
settings.REGISTER_WIN_SERVICE_WITH_PASS = obj.v_json
1 change: 1 addition & 0 deletions apps/node_man/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class KeyEnum(Enum):
"""枚举全局配置KEY,避免散落在各处难以维护"""

USE_TJJ = "USE_TJJ" # 是否启用TJJ
REGISTER_WIN_SERVICE_WITH_PASS = "REGISTER_WIN_SERVICE_WITH_PASS" # 是否使用密码注册windows服务
CONFIG_POLICY_BY_SOPS = "CONFIG_POLICY_BY_SOPS" # 是否使用标准运维自动开通网络策略
CONFIG_POLICY_BY_TENCENT_VPC = "CONFIG_POLICY_BY_TENCENT_VPC" # 是否使用腾讯云SDK自动开通网络策略
APIGW_PUBLIC_KEY = "APIGW_PUBLIC_KEY" # APIGW公钥,从PaaS接口获取或直接配到settings中
Expand Down
57 changes: 52 additions & 5 deletions apps/utils/encrypt/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
"""

import base64
from enum import Enum
from functools import wraps
from typing import Any, List, Optional, Tuple, Union

from Cryptodome import Util
from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.Cipher import PKCS1_v1_5 as PKCS1_v1_5_cipher
from Cryptodome.Hash import SHA1
from Cryptodome.PublicKey import RSA
Expand All @@ -22,6 +25,39 @@
ENCODING = "utf-8"


class CipherPadding(Enum):
"""填充标志"""

PKCS1 = "PKCS1"
PKCS1_OAEP = "PKCS1_OAEP"


class KeyObjType(Enum):
"""密钥对象类型"""

PRIVATE_KEY_OBJ = "private_key_obj"
PUBLIC_KEY_OBJ = "public_key_obj"


def public_key_obj_checker(key_obj_type: str):
"""
密钥对象检查器
:param key_obj_type: KeyObjType
:return:
"""

def decorate(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not getattr(self, key_obj_type):
raise ValueError(f"{key_obj_type} must be set if you want to call {func.__name__}")
return func(self, *args, **kwargs)

return wrapper

return decorate


def generate_keys() -> Tuple[str, str]:
"""
生成公私钥
Expand All @@ -40,18 +76,20 @@ def generate_keys() -> Tuple[str, str]:

class RSAUtil:
public_key_obj: RSA.RsaKey = None
private_key_obj: RSA.RsaKey = None
private_key_obj: Optional[RSA.RsaKey] = None

@staticmethod
def load_key(extern_key: Optional[Union[str, bytes]] = None, extern_key_file: Optional[str] = None) -> RSA.RsaKey:
def load_key(
extern_key: Optional[Union[str, bytes]] = None, extern_key_file: Optional[str] = None
) -> Optional[RSA.RsaKey]:
"""
导入rsa密钥
:param extern_key: 密钥内容
:param extern_key_file: 密钥文件,
:return:
"""
if not (extern_key or extern_key_file):
raise ValueError("key or key_file need to provide at least one.")
return None

if extern_key_file:
try:
Expand Down Expand Up @@ -93,10 +131,16 @@ def __init__(
private_extern_key: Optional[Union[str, bytes]] = None,
public_extern_key_file: Optional[str] = None,
private_extern_key_file: Optional[str] = None,
padding: str = CipherPadding.PKCS1.value,
):
self.public_key_obj = self.load_key(public_extern_key, public_extern_key_file)
self.private_key_obj = self.load_key(private_extern_key, private_extern_key_file)
if padding == CipherPadding.PKCS1_OAEP.value:
self.cipher_method = PKCS1_OAEP
else:
self.cipher_method = PKCS1_v1_5_cipher

@public_key_obj_checker(KeyObjType.PUBLIC_KEY_OBJ.value)
def encrypt(self, message: str) -> str:
"""
加密
Expand All @@ -106,12 +150,13 @@ def encrypt(self, message: str) -> str:
message_bytes = message.encode(encoding=ENCODING)
encrypt_message_bytes = b""
block_size = self.get_block_size(self.public_key_obj)
cipher = PKCS1_v1_5_cipher.new(self.public_key_obj)
cipher = self.cipher_method.new(self.public_key_obj)
for block in self.block_list(message_bytes, block_size):
encrypt_message_bytes += cipher.encrypt(block)
encrypt_message = base64.b64encode(encrypt_message_bytes)
return encrypt_message.decode(encoding=ENCODING)

@public_key_obj_checker(KeyObjType.PRIVATE_KEY_OBJ.value)
def decrypt(self, encrypt_message: str) -> str:
"""
解密
Expand All @@ -121,11 +166,12 @@ def decrypt(self, encrypt_message: str) -> str:
decrypt_message_bytes = b""
encrypt_message_bytes = base64.b64decode(encrypt_message)
block_size = self.get_block_size(self.private_key_obj, is_encrypt=False)
cipher = PKCS1_v1_5_cipher.new(self.private_key_obj)
cipher = self.cipher_method.new(self.private_key_obj)
for block in self.block_list(encrypt_message_bytes, block_size):
decrypt_message_bytes += cipher.decrypt(block, "")
return decrypt_message_bytes.decode(encoding=ENCODING)

@public_key_obj_checker(KeyObjType.PRIVATE_KEY_OBJ.value)
def sign(self, message: str) -> bytes:
"""
根据私钥和需要发送的信息生成签名
Expand All @@ -137,6 +183,7 @@ def sign(self, message: str) -> bytes:
signature = cipher.sign(sha)
return base64.b64encode(signature)

@public_key_obj_checker(KeyObjType.PUBLIC_KEY_OBJ.value)
def verify(self, message: str, signature: bytes):
"""
使用公钥验证签名
Expand Down
3 changes: 3 additions & 0 deletions apps/utils/tests/test_encrypt/test_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ def test_rsa_util__verify(self):
message = "验证私钥签名,如果验证结果为True,表明该消息从私钥拥有方发出,没有被修改"
signature = rsa_util.sign(message=message)
self.assertTrue(rsa_util.verify(message=message, signature=signature))

def test_key_obj_check_failed(self):
self.assertRaises(ValueError, rsa.RSAUtil().encrypt, "test")
4 changes: 3 additions & 1 deletion config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,9 @@ class StorageType(Enum):
# 下载文件路径
EXPORT_PATH = os.path.join(PUBLIC_PATH, "export")

# 脚本工具存放位置
BK_SCRIPTS_PATH = os.path.join(PROJECT_ROOT, "script_tools")

# ==============================================================================
# 后台配置
# ==============================================================================
Expand Down Expand Up @@ -509,7 +512,6 @@ def get_standard_redis_mode(cls, config_redis_mode: str, default: Optional[str]
}
}
BK_OFFICIAL_PLUGINS_INIT_PATH = os.path.join(PROJECT_ROOT, "official_plugin")
BK_SCRIPTS_PATH = os.path.join(PROJECT_ROOT, "script_tools")
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [
"apps.utils.drf.CsrfExemptSessionAuthentication",
]
Expand Down
9 changes: 9 additions & 0 deletions script_tools/gse_public_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvDM7NP/GYXJUURUemBdA
DUMWj8XKXq9UMfJFs2gWVpICW/A2/VqcgJbeVCauoy5hQ3og0/FqMOFsUrKakT8r
Kz5MPMDix9+5dCjEZEHz4Oxd+/gX77S1sMPIjlDnyj0AuwKAQ0hN+oPCrRp5wzVM
e11R+9VQxuEWCScXeEmcs39AFqQZHTXl6Ao3l/NXE2lsYuDB+AQiy276nnEkgmch
O7BgundCL3pvq9yZblEkTI3v1sSMYZaMU81YUOOZuO24YIqPwN/ZEYf7I4UkwO+w
MM8OozzsHzhJymI/oM9C6VvaecwY3L6fPTe9WWMqH84uOOaH6QuTXQ8jH2Ge8J7K
PwIDAQAB
-----END PUBLIC KEY-----
Loading

0 comments on commit c3bc0cc

Please sign in to comment.