Configuration management
本文将详细记录一次重构过程:将一个只能加载配置文件的 Python gRPC 客户端,升级为一个支持多种操作(如查询、设置、删除硬件和软件信息)的命令行接口(CLI)工具。
起点:一个简单的配置文件加载脚本
我们最初有一个名为 load_profile.py 的脚本。它的目标很明确:读取一个 JSON 格式的配置文件,并通过 gRPC 将其发送到服务器。其核心逻辑大致如下:
# 旧版 load_profile.py 逻辑
def main():
# 1. 解析命令行参数(JSON路径,服务器地址)
# 2. 检查文件是否存在
# 3. 创建一个 ProfileClient 实例
# 4. 连接到服务器
# 5. 调用 client.load_and_set_profile()
# 6. 断开连接
这个脚本能很好地完成它的本职工作,但它的功能太单一了。为了进行其他操作,比如查询硬件信息或擦除数据,我们需要一个更灵活、功能更丰富的工具。
目标:扩展客户端功能
我们的目标是让客户端支持以下五种核心操作,使其与 C# 版本的功能对齐:
create-config: 加载并设置一个完整的 TIB(Test Interface Board)配置文件。get-hw: 从服务器获取所有硬件信息。set-hw: 从 JSON 文件中读取硬件信息并将其设置到服务器。get-sw: 从服务器获取所有软件信息。erase-hw: 根据名称擦除指定的硬件信息。
为了实现这一目标,我们对核心的 profile_client.py 文件进行了两部分重大改造:在 ProfileClient 类中添加功能方法 和 使用 argparse 构建多命令 CLI 入口。
第一步:在 ProfileClient 中实现核心功能
所有与 gRPC 服务器的交互逻辑都封装在 ProfileClient 类中。我们为上述每个新功能都在这个类中添加了对应的方法。
代码实现
以下是我们添加到 profile_client.py 中的新方法:
# /mnt/c/Users/ezhuzix/repo_o/nib_application/grpc/profile_client.py
class ProfileClient:
# ... (已有的 connect, disconnect, load_and_set_profile 等方法) ...
def get_hw_infos(self):
"""封装 GetHWInfos gRPC 调用"""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return None
timestamp = Timestamp()
timestamp.FromSeconds(int(time.time()))
req = pb2.GetHWInfoRequest(
session=type_pb2.SessionId(
device_id=uuid.uuid4().hex,
start_time=timestamp
),
dut_position=1,
hw_name_mask=""
)
return self.config_stub.GetHWInfos(req, timeout=10)
def set_hw_infos(self, hw_info_list):
"""封装 SetHWInfo gRPC 调用"""
# ... 类似地构建 SetHWInfoRequest 并发送 ...
def get_sw_infos(self):
"""封装 GetSWInfos gRPC 调用"""
# ... 类似地构建 GetSWInfoRequest 并发送 ...
def erase_hw_info(self, hw_name):
"""封装 EraseHwInfo gRPC 调用"""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return None
timestamp = Timestamp()
timestamp.FromSeconds(int(time.time()))
req = pb2.EraseHWInfoRequest(
session=type_pb2.SessionId(
device_id=uuid.uuid4().hex,
start_time=timestamp
),
dut_position=1,
hw_name_to_erase=hw_name
)
return self.config_stub.EraseHwInfo(req, timeout=10)
代码解析:一次关于 Protobuf 的调试之旅
在实现这些功能时,我们遇到了一个典型的 Protobuf 使用问题。最初,我们尝试使用 pb2.Session(id=...) 来创建会话标识符,但这导致了 AttributeError 和 ValueError。
AttributeError: module 'test_interface_pb2' has no attribute 'Session': 这个错误告诉我们Session消息并不在test_interface_pb2模块中。通过检查.proto源文件,我们发现它实际上名为SessionId,并且定义在type.proto文件中。ValueError: Protocol message SessionId has no "id" field.: 修正了模块后,我们遇到了新问题。代码尝试为SessionId设置一个id字段,但.proto定义显示,它需要的字段是device_id和start_time。
// 在 type.proto 中 SessionId 的真实定义
message SessionId {
string device_id = 1;
google.protobuf.Timestamp start_time = 2;
}
最终的解决方案 是导入正确的模块 (type_pb2) 和 Timestamp 消息,并使用正确的字段名来构建请求。这个调试过程提醒我们,在使用 Protobuf 时,.proto 文件是最终的“真相来源”。
第二步:使用 argparse 构建多命令 CLI
在 ProfileClient 具备了所有能力之后,我们需要一个用户友好的方式来调用这些能力。Python 内置的 argparse 库是构建命令行工具的完美选择。我们重写了 load_profile.py 的主函数入口,将其变成一个功能丰富的 CLI 分发器。
代码实现
# /mnt/c/Users/ezhuzix/repo_o/nib_application/grpc/load_profile.py
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TestInterface Configuration CLI Tool")
parser.add_argument("--server", default="192.168.2.61:50050", help="Server address 'host:port'")
subparsers = parser.add_subparsers(dest="command", required=True)
# 为每个功能创建一个子命令
p_create = subparsers.add_parser("create-config", help="...")
p_create.add_argument("json_path", help="...")
p_get_hw = subparsers.add_parser("get-hw", help="...")
p_get_hw.add_argument("--output", help="...")
# ... (为 set-hw, get-sw, erase-hw 定义其他子命令) ...
args = parser.parse_args()
client = ProfileClient(server_address=args.server)
if not client.connect():
sys.exit(1)
try:
# 根据用户输入的命令,调用对应的方法
if args.command == "create-config":
# ... 调用 client.load_and_set_profile() ...
elif args.command == "get-hw":
hw_infos = client.get_hw_infos()
if hw_infos:
# 将 Protobuf 响应转换为 JSON 并打印
hw_list = json_format.MessageToDict(hw_infos).get("hwInfo", [])
output_json = json.dumps(hw_list, indent=2)
print(output_json)
elif args.command == "erase-hw":
response = client.erase_hw_info(args.hw_name)
if response and response.code == 0:
client.logger.info(f"Successfully erased...")
else:
client.logger.error(f"Failed to erase: {response.message}")
# ... (处理其他命令) ...
finally:
client.disconnect()
代码解析
subparsers = parser.add_subparsers(...): 这是argparse的核心功能,它允许我们创建多个独立的子命令,每个子命令都可以有自己的一套参数。dest="command": 这个参数非常重要,它告诉argparse将用户选择的子命令名称(如 "get-hw")存储在args.command属性中。- 逻辑分发: 主
try块中的if/elif链条根据args.command的值,精确地调用ProfileClient中对应的方法,并将命令行参数(如args.hw_name)传递给它。 - 优雅的输出: 对于
get-hw和get-sw命令,我们使用了google.protobuf.json_format将服务器返回的 Protobuf 对象转换成易于阅读和处理的 JSON 格式,极大地提升了可用性。
附录
profile_client.py
import os
import sys
import grpc
import logging
import json
import re
import datetime
# from google.protobuf import text_format
from google.protobuf import json_format
from google.protobuf.timestamp_pb2 import Timestamp # ADDED
# add the path to the generated proto files
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(current_dir, 'proto_python'))
# Now we can import the generated files
try:
import test_interface_pb2 as pb2
import service_ms_test_interface_pb2_grpc as pb2_grpc
import common_enums_pb2
import type_pb2
except ImportError:
print(f"Error: Could not import the generated protobuf files.")
sys.exit(1)
class ProfileClient:
"""
A client for managing device profiles via gRPC.
"""
def __init__(self, server_address='localhost:50051'):
self.server_address = server_address
self.channel = None
self.mmi_stub = None
self.config_stub = None
self.logger = logging.getLogger(self.__class__.__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
def connect(self):
"""Establishes a connection to the gRPC server."""
try:
self.channel = grpc.insecure_channel(self.server_address)
self.mmi_stub = pb2_grpc.TestInterfaceMMIStub(self.channel)
self.config_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
self.logger.info(f"Successfully connected to {self.server_address}")
return True
except grpc.RpcError as e:
self.logger.error(f"Failed to connect: {e}")
return False
def disconnect(self):
"""Closes the gRPC channel."""
if self.channel:
self.channel.close()
self.logger.info("Disconnected from the server.")
def _pascal_to_snake(self, name):
# Converts a PascalCase or camelCase string to UPPER_SNAKE_CASE.
# e.g., "EnterDutBootModeInternal" -> "ENTER_DUT_BOOT_MODE_INTERNAL"
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).upper()
def _get_proto_enum_string(self, json_value, enum_type_name, enum_keys):
"""
Constructs the full protobuf enum string from a JSON value by mapping naming conventions.
Handles special cases for numeric values and voltage strings.
"""
# Special mappings for enum types that need custom conversion
special_maps = {
"UartBaudrate": {
"115200": "BAUD_115200",
"57600": "BAUD_57600",
"9600": "BAUD_9600",
# add more as needed
},
"UartDatabits": {
"8": "EIGHT",
"7": "SEVEN",
},
"UartStopbits": {
"1": "ONE",
"2": "TWO",
},
"IOVoltageLevel": {
"V3P3": "V_3P3",
"V1P8": "V_1P8",
"V9P0": "V_9P0",
},
"SoftwareType": { # ADDED for software type mapping
"pboot": "PBOOT",
"uboot": "UBOOT",
"ubootenv": "UBOOT_ENV",
"boardparameters": "BOARD_PARAMETERS",
"faap": "FAAP",
"InitialFlashImage": "INITIAL_FLASH_IMAGE",
"SlotContentTable": "SLOT_CONTENT_TABLE",
"Sboot": "SBOOT",
"XcsConfig": "XCS_CONFIG",
"Productionparameters": "PRODUCTION_PARAMETERS",
}
}
if not isinstance(json_value, str) or not json_value:
# Default to UNSPECIFIED
snake_enum_type = self._pascal_to_snake(enum_type_name) # Convert enum type name to snake
unspecified = f"{snake_enum_type}_UNSPECIFIED"
self.logger.warning(f"No JSON value for {enum_type_name}, defaulting to {unspecified}")
return unspecified
# Check if a special mapping exists
if enum_type_name in special_maps and json_value in special_maps[enum_type_name]:
snake_json_value = special_maps[enum_type_name][json_value]
else:
# Convert the JSON value to snake case
if json_value.isupper() or json_value.islower():
snake_json_value = json_value.upper()
else:
snake_json_value = self._pascal_to_snake(json_value)
# Construct the full enum name
# Convert enum type name to snake case (e.g., "UartBaudrate" -> "UART_BAUDRATE")
if enum_type_name.startswith("Uart"):
snake_enum_type = "UART_" + enum_type_name[4:].upper()
else:
snake_enum_type = self._pascal_to_snake(enum_type_name)
full_name = f"{snake_enum_type}_{snake_json_value}"
# Verify the constructed name exists in the enum keys
if full_name in enum_keys:
return full_name
# Fallback: try without the type prefix (for cases where the value already includes it)
if snake_json_value in enum_keys:
return snake_json_value
# Try to find a key that ends with the snake_json_value
for key in enum_keys:
if key.endswith(f"_{snake_json_value}"):
return key
self.logger.warning(f"Could not map JSON value '{json_value}' to an enum in {enum_type_name}. Defaulting to {snake_enum_type}_UNSPECIFIED")
return f"{snake_enum_type}_UNSPECIFIED"
def load_and_set_profile(self, file_path, dut_position=1):
"""Loads all hardware profiles from a JSON file and sends them to the server."""
if not self.mmi_stub or not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return
self.logger.info(f"Loading all profiles from {file_path}")
try:
with open(file_path, 'r') as f:
content = re.sub(r"//.*", "", f.read())
full_config = json.loads(content)
except (FileNotFoundError, json.JSONDecodeError) as e:
self.logger.error(f"Failed to read or parse profile file: {e}")
return
# Correctly navigate the nested JSON structure
test_interface_config = full_config.get('TestInterfaceConfiguration')
if not test_interface_config:
self.logger.error("No 'TestInterfaceConfiguration' found in the JSON file.")
return
# The profile group (e.g., 'AIR1672') is the first value in the TestInterfaceConfiguration dict
profile_group = next(iter(test_interface_config.values()), None)
if not profile_group:
self.logger.error("No profile group (e.g., 'AIR1672') found inside 'TestInterfaceConfiguration'.")
return
# The base name is the key of the profile group
profile_base_name = next(iter(test_interface_config.keys()), "UnknownProfile")
profile_request = pb2.SetProfileRequest(dut_position=dut_position)
# ADDED: Create and set session
now = datetime.datetime.now()
timestamp = Timestamp()
timestamp.FromDatetime(now)
profile_request.session.device_id = "MMI"
profile_request.session.start_time.CopyFrom(timestamp)
# profile_group is now a dict like {'TIB2': {...}, 'TIB3': {...}}
for hw_target, profile_details in profile_group.items():
if not isinstance(profile_details, dict):
self.logger.warning(f"Skipping item '{hw_target}' as it's not a valid profile object.")
continue
self.logger.info(f"Building profile for HW target '{hw_target}'")
one_profile = profile_request.profiles.add()
one_profile.name = profile_base_name
one_profile.hw_target = hw_target
# --- IO Map (aliases) ---
if 'IOConfigurations' in profile_details:
for config in profile_details['IOConfigurations']:
io_map = one_profile.io_map.io_maps.add()
io_map.alias = config.get('Alias', '')
io_map.actual_name = config.get('ActualName', '')
# state is included only if True (default False will be omitted in text format)
if config.get('State', False):
io_map.state = True
# --- UART Map (aliases) ---
if 'UartConfigurations' in profile_details:
for config in profile_details['UartConfigurations']:
uart_map = one_profile.uart_map.uart_maps.add()
uart_map.alias = config.get('Alias', '')
uart_map.actual_name = config.get('ActualName', '')
# --- Voltage Rail Map (aliases) ---
if 'VoltageRailConfigurations' in profile_details:
for config in profile_details['VoltageRailConfigurations']:
vr_map = one_profile.voltage_rail_map.voltage_rail_maps.add()
vr_map.alias = config.get('Alias', '')
vr_map.actual_name = config.get('ActualName', '')
# --- Fan Map (aliases) ---
if 'FanConfigurations' in profile_details:
for config in profile_details['FanConfigurations']:
fan_map = one_profile.fan_map.fan_maps.add()
fan_map.alias = config.get('Alias', '')
fan_map.actual_name = config.get('ActualName', '')
# --- Trigger Map (aliases) --- ADDED
if 'TriggerConfigurations' in profile_details:
for config in profile_details['TriggerConfigurations']:
trigger_map = one_profile.trigger_map.trigger_maps.add()
trigger_map.alias = config.get('Alias', '')
trigger_map.actual_name = config.get('ActualName', '')
# --- External Alarm Map (aliases) --- ADDED
if 'ExternalAlarmConfigurations' in profile_details:
for config in profile_details['ExternalAlarmConfigurations']:
ea_map = one_profile.external_alarm_map.external_alarm_maps.add()
ea_map.alias = config.get('Alias', '')
ea_map.actual_name = config.get('ActualName', '')
# --- IO Configuration (initial settings) ---
if 'IOConfigurationInitialSetting' in profile_details:
for config in profile_details['IOConfigurationInitialSetting']:
io_config = one_profile.io_configuration.data.add()
io_config.name = config.get('Name', '')
# IoDirection
dir_str = config.get('IoDirection')
if dir_str:
io_config.io_direction = common_enums_pb2.IODirection.Value(
self._get_proto_enum_string(dir_str, "IoDirection", common_enums_pb2.IODirection.keys())
)
# IoVoltageLevel
volt_str = config.get('IoVoltageLevel')
if volt_str:
io_config.io_voltage_level = common_enums_pb2.IOVoltageLevel.Value(
self._get_proto_enum_string(volt_str, "IOVoltageLevel", common_enums_pb2.IOVoltageLevel.keys())
)
# --- UART Configuration (initial settings) ---
if 'UartConfigurationInitialSetting' in profile_details:
for config in profile_details['UartConfigurationInitialSetting']:
uart_config = one_profile.uart_configuration.data.add()
uart_config.name = config.get('Name', '')
uart_config.port_number = config.get('PortNumber', 0)
# Baudrate
baud_str = config.get('Baudrate')
if baud_str:
uart_config.baudrate = common_enums_pb2.UartBaudrate.Value(
self._get_proto_enum_string(baud_str, "UartBaudrate", common_enums_pb2.UartBaudrate.keys())
)
# Parity
parity_str = config.get('Parity')
if parity_str:
uart_config.parity = common_enums_pb2.UartParity.Value(
self._get_proto_enum_string(parity_str, "UartParity", common_enums_pb2.UartParity.keys())
)
# Databits
databits_str = config.get('Databits')
if databits_str:
uart_config.databits = common_enums_pb2.UartDatabits.Value(
self._get_proto_enum_string(databits_str, "UartDatabits", common_enums_pb2.UartDatabits.keys())
)
# Stopbits
stopbits_str = config.get('Stopbits')
if stopbits_str:
uart_config.stopbits = common_enums_pb2.UartStopbits.Value(
self._get_proto_enum_string(stopbits_str, "UartStopbits", common_enums_pb2.UartStopbits.keys())
)
# Hardware Interface (using UartHardwareInterface)
hw_if_str = config.get('HardwareInterface')
if hw_if_str:
uart_config.hardware_interface = common_enums_pb2.UartHardwareInterface.Value(
self._get_proto_enum_string(hw_if_str, "UartHardwareInterface", common_enums_pb2.UartHardwareInterface.keys())
)
# Voltage Level (optional, for some UARTs)
volt_str = config.get('VoltageLevel')
if volt_str:
uart_config.voltage_level = common_enums_pb2.IOVoltageLevel.Value(
self._get_proto_enum_string(volt_str, "IOVoltageLevel", common_enums_pb2.IOVoltageLevel.keys())
)
# --- Voltage Rail Configuration (initial settings) --- MODIFIED (was VoltageRegulatorInitialSetting)
if 'VoltageRailConfigurationInitialSetting' in profile_details:
for config in profile_details['VoltageRailConfigurationInitialSetting']:
vr_config = one_profile.voltage_rail_configuration.data.add()
vr_config.name = config.get('Name', '')
vr_config.voltage = float(config.get('Voltage', 0.0))
# --- Boot Mode Configurations ---
if 'BootModeConfigurations' in profile_details:
for config in profile_details['BootModeConfigurations']:
boot_mode = one_profile.boot_mode_config.boot_mode_configs.add()
mode_str = config.get('Mode')
if mode_str:
boot_mode.mode = common_enums_pb2.BootModeSelection.Value(
self._get_proto_enum_string(mode_str, "BootModeSelection", common_enums_pb2.BootModeSelection.keys())
)
if 'IOSettings' in config:
for io_setting in config['IOSettings']:
io_setting_msg = boot_mode.io_settings.add()
io_setting_msg.io_name = io_setting.get('IoName', '')
# state is included only if True
if io_setting.get('State', False):
io_setting_msg.state = True
# --- Software Area Configurations --- MODIFIED (now uses _get_proto_enum_string)
if 'SoftwareAreaConfigurations' in profile_details:
for config in profile_details['SoftwareAreaConfigurations']:
sw_area = one_profile.software_repos.software_area_configurations.add()
sw_type_str = config.get('SwType')
if sw_type_str:
sw_area.sw_type = common_enums_pb2.SoftwareType.Value(
self._get_proto_enum_string(sw_type_str, "SoftwareType", common_enums_pb2.SoftwareType.keys())
)
# Addresses are hex strings, convert to int
sw_area.start_address = int(config.get('StartAddress', '0'), 16)
sw_area.area_size = int(config.get('AreaSize', '0'), 16)
sw_area.erase_start_address = int(config.get('EraseStartAddress', '0'), 16)
sw_area.erase_area_size = int(config.get('EraseAreaSize', '0'), 16)
# File info (if present)
if 'File' in config:
file_info = config['File']
sw_area.file.name = file_info.get('Name', '')
sw_area.file.description = file_info.get('Description', '')
sw_area.file.version = file_info.get('Version', '')
sw_area.file.path = file_info.get('Path', '')
if not profile_request.profiles:
self.logger.error("Failed to build any profiles from the file.")
return
self.logger.info("Sending SetProfile request...")
self.logger.debug(f"Request payload: {profile_request}")
try:
response = self.config_stub.SetProfile(profile_request)
self.logger.info(f"Request content: {json_format.MessageToJson(profile_request)}")
if response.code == 0:
self.logger.info("Successfully set profile.")
self.logger.info(f"Server response: {response}")
else:
self.logger.error(f"Failed to set profile. Response: {response}")
except grpc.RpcError as e:
self.logger.error(f"An RPC error occurred: {e.details()} (code: {e.code().name})")
def get_hw_infos(self):
"""Wrapper for GetHWInfos gRPC call."""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return None
now = datetime.datetime.now()
timestamp = Timestamp()
timestamp.FromDatetime(now)
req = pb2.GetHWInfoRequest(
session=type_pb2.SessionId(
device_id=uuid.uuid4().hex,
start_time=timestamp
),
dut_position=1,
hw_name_mask=""
)
return self.config_stub.GetHWInfos(req, timeout=10)
def set_hw_infos(self, hw_info_list):
"""Wrapper for SetHWInfo gRPC call for a list of HW info."""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return
for hw_info_dict in hw_info_list:
hw_info = pb2.HWInfo()
json_format.ParseDict(hw_info_dict, hw_info)
now = datetime.datetime.now()
timestamp = Timestamp()
timestamp.FromDatetime(now)
req = pb2.SetHWInfoRequest(
session=type_pb2.SessionId(
device_id=uuid.uuid4().hex,
start_time=timestamp
),
dut_position=1,
hw_info=hw_info
)
self.logger.info(f"Setting HW Info for: {hw_info.hw_name}")
resp = self.config_stub.SetHWInfo(req, timeout=10)
if resp.code != 0: # Assuming 0 is OK
self.logger.error(f"Failed to set HW Info for {hw_info.hw_name}: {resp.message}")
def get_sw_infos(self):
"""Wrapper for GetSWInfos gRPC call."""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return None
now = datetime.datetime.now()
timestamp = Timestamp()
timestamp.FromDatetime(now)
req = pb2.GetSWInfoRequest(
session=type_pb2.SessionId(
device_id=uuid.uuid4().hex,
start_time=timestamp
),
dut_position=1
)
return self.config_stub.GetSWInfos(req, timeout=10)
def erase_hw_info(self, hw_name):
"""Wrapper for EraseHwInfo gRPC call."""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return None
now = datetime.datetime.now()
timestamp = Timestamp()
timestamp.FromDatetime(now)
req = pb2.EraseHWInfoRequest(
session=type_pb2.SessionId(
device_id=uuid.uuid4().hex,
start_time=timestamp
),
dut_position=1,
hw_name_to_erase=hw_name
)
return self.config_stub.EraseHwInfo(req, timeout=10)
config_main.py
import argparse
import sys
import os
import json
from google.protobuf import json_format
from profile_client import ProfileClient
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="TestInterface Configuration CLI Tool")
parser.add_argument("--server", default="192.168.2.61:50050", help="Server address 'host:port'")
subparsers = parser.add_subparsers(dest="command", required=True)
# create-config command
p_create = subparsers.add_parser("create-config", help="Load a full TIB profile from a JSON file.")
p_create.add_argument("json_path", help="Path to the JSON profile file.")
# get-hw command
p_get_hw = subparsers.add_parser("get-hw", help="Get all hardware information from the server.")
p_get_hw.add_argument("--output", help="Path to save the output JSON file.")
# set-hw command
p_set_hw = subparsers.add_parser("set-hw", help="Set hardware information from a JSON file.")
p_set_hw.add_argument("input", help="Path to the input JSON file containing hardware info.")
# get-sw command
p_get_sw = subparsers.add_parser("get-sw", help="Get all software information from the server.")
p_get_sw.add_argument("--output", help="Path to save the output JSON file.")
# erase-hw command
p_erase_hw = subparsers.add_parser("erase-hw", help="Erase hardware information by name.")
p_erase_hw.add_argument("hw_name", help="The hardware name to erase.")
args = parser.parse_args()
client = ProfileClient(server_address=args.server)
if not client.connect():
sys.exit(1)
try:
if args.command == "create-config":
profile_full_path = os.path.abspath(args.json_path)
if not os.path.exists(profile_full_path):
print(f"Error: Profile file not found at '{profile_full_path}'")
else:
client.load_and_set_profile(profile_full_path)
elif args.command == "get-hw":
hw_infos = client.get_hw_infos()
if hw_infos:
hw_list = json_format.MessageToDict(hw_infos).get("hwInfo", [])
output_json = json.dumps(hw_list, indent=2)
if args.output:
with open(args.output, "w") as f:
f.write(output_json)
client.logger.info(f"Hardware info saved to {args.output}")
else:
print(output_json)
elif args.command == "set-hw":
try:
with open(args.input, "r") as f:
hw_data = json.load(f)
client.set_hw_infos(hw_data)
client.logger.info(f"Successfully sent hardware info from {args.input}")
except FileNotFoundError:
client.logger.error(f"Input file not found: {args.input}")
except json.JSONDecodeError:
client.logger.error(f"Could not decode JSON from {args.input}")
elif args.command == "get-sw":
sw_infos = client.get_sw_infos()
if sw_infos:
sw_list = json_format.MessageToDict(sw_infos).get("swInfo", [])
output_json = json.dumps(sw_list, indent=2)
if args.output:
with open(args.output, "w") as f:
f.write(output_json)
client.logger.info(f"Software info saved to {args.output}")
else:
print(output_json)
elif args.command == "erase-hw":
response = client.erase_hw_info(args.hw_name)
if response and response.code == 0: # Assuming 0 is OK
client.logger.info(f"Successfully erased hardware info for '{args.hw_name}'.")
elif response:
client.logger.error(f"Failed to erase hardware info for '{args.hw_name}': {response.message}")
finally:
client.disconnect()
结论
通过Python类和命令行工具,实现了配置文件的批量下发、硬件/软件信息的查询与修改。新增的硬件/软件信息操作接口使得客户端功能更加完善,能够满足日常测试中对设备配置的动态管理需求。

浙公网安备 33010602011771号