Skip to content

Commit

Permalink
feature: Windows Agent安装支持用administrator用户注册服务(close #277)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangzhw8 authored and ZhuoZhuoCrayon committed Dec 8, 2021
1 parent 905d097 commit ef631b3
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 134 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
53 changes: 48 additions & 5 deletions apps/backend/tests/components/collections/agent/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import time

from django.conf import settings
from django.test import TestCase
from django.test import TestCase, override_settings
from mock import patch

from apps.backend.agent.tools import gen_commands
Expand Down 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):
)


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

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()


@override_settings(REGISTER_WIN_SERVICE_WITH_PASS=True)
class InstallWindowsPAgentWithEncryptedPasswordSuccessTest(InstallPAgentSuccessTest):
def setUp(self):
super().setUp()
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
59 changes: 53 additions & 6 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 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 @@ -39,19 +75,21 @@ def generate_keys() -> Tuple[str, str]:


class RSAUtil:
public_key_obj: RSA.RsaKey = None
private_key_obj: RSA.RsaKey = None
public_key_obj: Optional[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

@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)

@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)

@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)

@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
Loading

0 comments on commit ef631b3

Please sign in to comment.