网络自动化之ZTP自动装机(H3C)
1、H3C的ZTP脚本华三#!usr/bin/pythonimport comwarefrom time import strftime,gmtime,sleepimport signalimport osimport stringimport commandsimport hashlib#U can use 2 modes to obtain the config file#- 'python_
·
1、H3C的ZTP脚本
华三
#!usr/bin/python
import comware
from time import strftime,gmtime,sleep
import signal
import os
import string
import commands
import hashlib
#U can use 2 modes to obtain the config file
#- 'python_static'
#- 'python_serial_number'
python_config_file_mode = "python_serial_number"
#Required space to copy config kickstart and system image in KB
required_space = 150000
#Enable IRF function
#- True
#- False
irf = True
#transfer information,TFTP传输的用户名密码,可以默认匿名登陆,只下载不上传
username="1"
password="123456"
hostname = "192.168.1.100"
protocol = "http" #可以使用FTP协议
vrf = ""
config_timeout = 120
irf_timeout = 120 #没有irf可以不用写??
image_timeout = 2100
#Server File Path,服务器目录信息
server_path = "" #
#Loacl File path,本地目录信息
local_path = "flash:/"
#cfg file path
cfg_dir = ""
#Local config name,配置文件本地存放名称
config_local_name = "config.cfg"
#Server config name,配置文件服务器存放名称
config_server_name = "config.cfg"
#Local boot name,版本启动文件本地存放名称
boot_local_name = ""
#Server boot name,版本文件服务器存放名称
boot_server_name = ""
#Local patch name,补丁启动文件本地存放名称
patch_local_name = ""
#Server patch name,补丁文件服务器存放名称
patch_server_name = ""
#Local irf name,SN文件信息本地存放名称,用于配置IRF member ID
irf_local_name = "sn.txt"
#Server irf name,SN文件信息服务器存放名称,用于IRF member ID
irf_server_name = "sn.txt"
#脚本文件执行日志存放位置。
python_log_name = ""
#device type
device = ""
#Local devcie version
local_version = ""
#Server target version
server_version = ""
#device infomation
device_list = {
"S6800-54QF":{
"version":"Release 2432P01",
"version_file":"S6800-CMW710-R2432P01.ipe",
"patch_list":[
"S6800-CMW710-SYSTEM-R2432P01H11.bin",
"S6800-CMW710-SYSTEM-WEAK-R2432P01H16.bin"
],
"cfg_dir":"",
"irf_server_name":"",
"irf":False
},
"S6820-56HF":{
"version":"Feature 6207",
"version_file":"S6820-CMW710-F6207.ipe",
"patch_list":[
],
"cfg_dir":"cfg/",
"irf_server_name":"",
"irf":False
},
"S5820V2-54QS-GE":{
"version":"Feature 6207",
"version_file":"",
"patch_list":[
],
"cfg_dir":"",
"irf_server_name":"sn.txt",
"irf":False
}
}
#Write Log File
def write2Log(info):
global python_log_name, local_path
if python_log_name == "":
try:
python_log_name = "%spython_%s" %(local_path, "ztp.log")
except Exception as inst:
print inst
fd = open(python_log_name, "a")
fd.write(info)
fd.flush()
fd.close()
#get device infomation
def get_version():
cmd_output = comware.CLI("display version",False).get_output()
for line in cmd_output:
if "Comware Software" in line:
version = line.split(",")[-1].strip()
return version
write2Log("\nGet version failure.")
print "\n###Get version failure.###"
return False
def check_patch(patch):
cmd_output = comware.CLI("display install active",False).get_output()
for cmd_line in cmd_output:
if (patch.upper() in cmd_line) or (patch.lower() in cmd_line):
return True
return False
def get_device():
cmd_output = comware.CLI("display version",False).get_output()
for line in cmd_output:
if "uptime" in line:
device = line.split(" ")[1].strip()
print device
return device
write2Log("\nGet device type failure.")
print "\n###Get device type failure###"
return False
def get_device_info():
global device,local_version,server_version,boot_local_name,boot_server_name
global patch_server_name,irf,cfg_dir,irf_server_name
local_version = get_version()
if local_version == False:
return False
device = get_device()
if device == False :
write2Log("\nThe %s is not in the list!" %device)
print "\n###The %s is not in the list!###" %device
return False
if device_list.get(device) != None:
device_dic = device_list.get(device)
server_version = device_dic.get("version")
boot_local_name = device_dic.get("version_file")
boot_server_name = device_dic.get("version_file")
patch_server_name = device_dic.get("patch_list")
cfg_dir = device_dic.get("cfg_dir")
irf_server_name = device_dic.get("irf_server_name")
irf = device_dic.get("irf")
else:
write2Log("\nThe %s is not in the list!" %device)
print "\n###The %s is not in the list!###" %device
return False
return True
def check_version():
if boot_server_name == "":
print "\n###boot_server_name is None,No nedd Check Version###"boot_local_name
return True
if local_version == server_version:
write2Log("\nThe version is consistent.")
print "\n###The version is consistent.###"
return True
write2Log("\nThe version is different,Please download later.Target version %s" %server_version)
print "\n###The version is different,Please download later.Target version %s###" %server_version
return False
########################################################################################################
# get path according to the Chassis and Slot,貌似在获取交换机上的目录
def getPath(chassisID, slotID):
global local_path
path = ""
obj = comware.get_self_slot()
if (obj[0] == chassisID) and (obj[1] == slotID):
return local_path
if chassisID != -1:
path = "chassis%d#" % chassisID
if slotID != -1:
path = "%sslot%d#%s" %(path, slotID, local_path)
return path
#Remove File,移动文件夹
def removeFile(filename):
try:
os.remove(filename)
except os.error:
pass
#Cleanup one device temp files,清除设备配置文件、版本文件、补丁、MD5文件
def cleanDeviceFiles(str, oDevNode):
global config_local_name, boot_local_name, irf_local_name
sFilePath = getPath(oDevNode[0], oDevNode[1])
if str == "error":
if config_local_name != "":
removeFile("%s%s" %(sFilePath, config_local_name))
for patch in patch_server_name:
removeFile("%s%s" %(sFilePath, patch))
if boot_local_name != "":
removeFile("%s%s" %(sFilePath, boot_local_name))
if irf_local_name != "":
removeFile("%s%s" %(sFilePath, irf_local_name))
removeFile("%s%s.md5" %(sFilePath, config_local_name))
removeFile("%s%s.md5" %(sFilePath, boot_local_name))
removeFile("%s%s.md5" %(sFilePath, irf_local_name))
for patch in patch_server_name:
removeFile("%s%s.md5" %(sFilePath, patch))
write2Log("\ndelete %s all files\n" %sFilePath)
print "\n###INFO:\ndelete %s all files###" %sFilePath
#Cleanup files,清除文件信息,主/备slot的文件信息清楚
def cleanupFiles(str):
aSlotRange = []
if ("get_standby_slot" in dir(comware)):
aSlotRange = aSlotRange + comware.get_standby_slot()
aSlotRange.append(comware.get_self_slot())
i = 0
while i < len(aSlotRange):
if(aSlotRange[i] != None):
cleanDeviceFiles(str, aSlotRange[i])
i = i + 1
#Verify if free space is available to download config, boot-loader image,检查剩余空间是否足够
def verifyfreespace(path):
global required_space
try:
s = os.statvfs(path)
freespace = (s.f_bavail * s.f_frsize) /1024
write2Log("\nthe %s free space is %s" %(path, freespace))
print "\n###the %s free space is %s KB###" %(path, freespace)
if required_space > freespace:
write2Log("\nthe %s space is not enough" % path)
print "\n####the %s space is not enough####" % path
return False
except Exception as inst:
write2Log("\nverify %s free space exception: %s" % (path, inst))
print "\n###verify %s free space exception: %s###" % (path, inst)
return False
return True
#verify device freespace,确认设备剩余空间
def verifyDeviceFree(obj):
path = getPath(obj[0], obj[1])
if True != verifyfreespace(path):
return False
return True
#check all mpu free space,检查所有的slot空间
def verifyAllFreeSpace():
aSlotRange = []
if ("get_standby_slot" in dir(comware)):
aSlotRange = aSlotRange + comware.get_standby_slot()
aSlotRange.append(comware.get_self_slot())
bAllEnough = True
i = 0
while i < len(aSlotRange):
if(aSlotRange[i] != None) and (True != verifyDeviceFree(aSlotRange[i])):
bAllEnough = False
i = i + 1
return bAllEnough
#确认脚本是否正常,正常则清除所有文件,不包括版本和补丁;error的话则清除所有
def doExit(str):
if str == "success":
write2Log("\nThe script is running success!")
print "\n#### The script is running success! ####"
cleanupFiles("success")
comd = "reboot force"
comware.CLI(comd, False)
exit(0)
if str == "error":
write2Log("\nThe script is running failed!")
print "\n#### The script is running failed! ####"
cleanupFiles("error")
exit(1)
else:
exit(0)
#get Chassis and Slot,获取主控板的槽位号
def getChassisSlot(style):
if style == "master":
obj = comware.get_self_slot()
if len(obj) <= 0:
write2Log("\nget %s chassis and slot failed" % style)
print "\n####get %s chassis and slot failed####" % style
return None
return obj
#获取启动软件包发生错误返回的信息
#signal terminal handler function
def sig_handler_no_exit(signum, function):
write2Log("\nSIGTERM Handler while configuring boot-loader variables")
print "\n####SIGTERM Handler while configuring boot-loader variables####"
#软件升级过程发生错误时返回的信息
#signal terminal handler
def sigterm_handler(signum, function):
write2Log("\nSIGTERM Handler")
print "\n####SIGTERM Handler####"
cleanupFiles("error")
doExit("error")
#从HTTP服务器上下载文件(后续用TFTP服务器)
#transfer file
def doCopyFile(src = "", des = "", login_timeout = 10):
global username, password, hostname, protocol, vrf
print "\n###INFO: Starting Copy of %s###" % src
try:
removeFile(des)
obj = comware.Transfer(protocol, hostname, src, des, vrf, login_timeout, username, password)
if obj.get_error() != None:
write2Log("\ncopy %s failed: %s" % (src, obj.get_error()))
print "\n####copy %s failed: %s####" % (src, obj.get_error())
return False
except Exception as inst:
write2Log("\ncopy %s exception: %s" % (src, inst))
print "\n####copy %s exception: %s####" % (src, inst)
return False
write2Log("\ncopy file %s to %s success" % (src, des))
print "\n###INFO: Completed Copy of %s###" % src
return True
#Get MD5SUM from md5ConfigFile,获取MDESUM
def getMD5SumGiven(keyword, filename):
try:
file = open(filename, "r")
line = file.readline()
while "" != line:
if not string.find(line, keyword, 0, len(keyword)):
line = line.split("=")
line = line[1]
line = line.strip()
file.close()
return line
line = file.readline()
file.close()
except Exception as inst:
write2Log("\nget %s md5 exception: %s" % (filename, inst))
print "\n####get %s mz'sd5 exception: %s####" % (filename, inst)
return ""
#verify MD5SUM of the file,MD5校验,计算出文件的md5值和给的md5进行对比
def verifyMD5sumofFile(md5sumgiven, filename):
if md5sumgiven == "":
write2Log("\nverify %s md5 error: the %s md5 file is error" %(filename, filename))
print "\n####verify %s md5 error: the %s md5 file is error####" %(filename, filename)
return False
try:
m = hashlib.md5()
f = open(filename, 'rb')
buffer = 8192
while 1:
chunk = f.read(buffer)
if not chunk:
break
m.update(chunk)
f.close()
md5calculated = m.hexdigest()
except Exception as inst:
write2Log("\nverify %s md5 exception: %s" % (filename, inst))
print "\n####verify %s md5 exception: %s####" % (filename, inst)
return False
if md5sumgiven == md5calculated:
return True
write2Log("\nverify %s md5 error: md5sumgiven is %s filemd5 is %s" %(filename, md5sumgiven, md5calculated))
print "\n####verify %s md5 error: md5sumgiven is %s filemd5 is %s####" %(filename, md5sumgiven, md5calculated)
return False
#Check MD5 file,检查MD5文件,这是要下载md5文件了吧?
def checkFile(src, dest):
src = "%s.md5" % src
destmd5 = "%s.md5" % dest
bFlag = doCopyFile(src, destmd5, 120)
if (True == bFlag) and (True == verifyMD5sumofFile(getMD5SumGiven("md5sum", destmd5), dest)):
write2Log("\ncheckFile success: %s" % destmd5)
print "\n####checkFile success: %s####" % destmd5
return True
elif (True != bFlag):
write2Log("\n%s is not exist! Don't verify the MD5 file!" % destmd5)
print "\n###INFO: %s is not exist! Don't verify the MD5 file!###" % destmd5
return True
return False
#Get config file according to the mode,取到设备配置文件名称
def getCfgFileName():
global config_server_name
if (python_config_file_mode == "python_serial_number") and (os.environ.has_key('DEV_SERIAL')):
config_server_name = "%sconf_%s.cfg" % (cfg_dir,os.environ['DEV_SERIAL'])
return config_server_name
else:
return config_server_name
#copy file to all standby slot,将文件copy到备的板卡上
def syncFileToStandby(sSrcFile, sFileName):
try:
aSlotRange = []
if ("get_standby_slot" in dir(comware)):
aSlotRange = aSlotRange + comware.get_standby_slot()
i = 0
while i < len(aSlotRange):
if(aSlotRange[i] != None):
sDestFile = "%s%s" %(getPath(aSlotRange[i][0], aSlotRange[i][1]), sFileName)
removeFile(sDestFile)
open(sDestFile,"wb").write(open(sSrcFile,"rb").read())
write2Log("\nsync file to standby %s" % (sDestFile))
print "\n####sync file to standby %s####" % (sDestFile)
i = i + 1
except Exception as inst:
write2Log("\nsync file to standby %s exception: %s" % (sSrcFile, inst))
print "\n####sync file to standby %s exception: %s####" % (sSrcFile, inst)
#Procedure to copy config file using global information
#根据脚本开始处定义的全局变量,下载启动软件包、配置文件、sn文件等
def copyAndCheckFile(src, dest, timeout):
global server_path, local_path
srcTmp = "%s%s" % (server_path, src)
sDestFile = "%s%s" % (local_path, dest)
if (True == doCopyFile(srcTmp, sDestFile, timeout)) and (True == checkFile(srcTmp, sDestFile)):
syncFileToStandby(sDestFile, dest)
return True
else:
srcTmp = "%sdefault_%s" %(server_path, src)
if (True == doCopyFile(srcTmp, sDestFile, timeout)) and (True == checkFile(srcTmp, sDestFile)):
syncFileToStandby(dest)
return True
return False
# split the Chassis and Slot,整合chassis_slot
def splitChassisSlot(chassisID, slotID):
chassis_slot = ""
if chassisID != -1:
chassis_slot = " chassis %d" % chassisID
if slotID != -1:
chassis_slot = "%s slot %d" %(chassis_slot, slotID)
return chassis_slot
#获取chassis号
def splitChassis(chassisID):
chassis = " "
if chassisID != -1:
chassis = " chassis %d" % chassisID
return chassis
#从服务器上下载启动软件包
def copyBootImage():
global image_timeout, local_path, boot_server_name, boot_local_name
if boot_server_name == "":
return True
src = "%s" % boot_server_name
return copyAndCheckFile(src, boot_local_name, image_timeout)
#从服务器上下载配置文件
def copyCfgFile():
global config_timeout, local_path, config_local_name
src = "%s" % getCfgFileName()
return copyAndCheckFile(src, config_local_name, config_timeout)
#下载irf_server_name和irf_local_name配置文件(sn.txt),函数中还检查member_id信息,将member_id统一改为1
def copyIrfStack():
global irf_timeout, local_path, irf_local_name, irf_server_name
if irf_server_name == "" or irf == False:
print "\n###irf_server_name is None,No need copy irf###"
if True != renumber_id():
return False
return True
src = "%s" % irf_server_name
return copyAndCheckFile(src, irf_local_name, config_timeout)
#执行boot-loader命令,升级交换机版本,为设备指定下次主用启动文件
# Procedure to Install Boot Image
def installBoot(chassis_slot, sFile, style):
result = None
write2Log("\ninstall%s%s begin" %(chassis_slot, style))
print "\nINFO: Install%s%s Start, Please Wait..." %(chassis_slot, style)
comd = "boot-loader file %s%s%s" % (sFile, chassis_slot, style)
try:
result = comware.CLI(comd, False)
if result == None:
write2Log("\nboot-loader file %s%s%s failed" % (sFile, chassis_slot, style))
print "\n####boot-loader file %s%s%s failed####" % (sFile, chassis_slot, style)
return False
except Exception as inst:
write2Log("\nboot-loader %s exception: %s" % (sFile, inst))
print "\n####boot-loader %s exception: %s####" % (sFile, inst)
return False
return True
###############################################################################################################
#Procedure to install boot image,升级交换机版本
def installBootImage():
global boot_local_name
if boot_server_name == "":
print "\n###boot_server_name is None,No need Install image###"
return True
aSlotRange = [comware.get_self_slot()]
if ("get_standby_slot" in dir(comware)):
aSlotRange = aSlotRange + comware.get_standby_slot()
bInstallOk = True
i = 0
while i < len(aSlotRange):
sFile = "%s%s" %(getPath(aSlotRange[0][0], aSlotRange[0][1]), boot_local_name)
if False == installBoot(splitChassisSlot(aSlotRange[i][0], aSlotRange[i][1]), sFile, " main"):
bInstallOk = False
i = i + 1
return bInstallOk
#Copy patch,下载补丁
def copy_patch():
global image_timeout, local_path, patch_server_name
if len(patch_server_name) == 0:
print "\n###INFO: Patch list is None,No need to copy patch###"
return True
print "\n###INFO: Download patch Start###"
for patch_name in patch_server_name:
src = "%s" % patch_name
copyAndCheckFile(src, src, image_timeout)
return True
#Install patch,安装补丁,并install commit使补丁下次设备启动还生效
def installPacth(chassis, sFile , patch):
result = ""
if True == check_patch(patch):
print "\n####INFO:The patch %s has been installed####" % patch
else:
try:
print "\n###INFO: Install patch %s###" % patch
commands = "install activate patch %s %s all" % (chassis,sFile)
print commands
comware.CLI(commands, False)
print "\n###INFO: Install patch %s success###" % patch
except Exception as inst:
write2Log("\nInstall patch %s filed.reason %s" % (patch, inst))
print "\n####INFO:Install patch %s filed. reason %s####" % (patch, inst)
return False
commands = " install commit"
comware.CLI(commands, False)
return True
def installPatchImage():安装补丁,循环调用安装程序
global patch_server_name
if len(patch_server_name) == 0:
print "\n####INFO:Install patch list is None,No need to install patch####"
return True
slot = comware.get_self_slot()
for patch in patch_server_name:
sFile = "%s%s" %(getPath(slot[0], slot[1]), patch)
if False == installPacth(splitChassis(slot[0]), sFile , patch):
return False
return True
指定设备启动文件
def startupCfg():
global local_path, config_local_name
result = None
dest = "%s%s" %(local_path, config_local_name)
write2Log("\nstartup saved-configuration %s begin" %dest)
print "\n###INFO: Startup Saved-configuration Start###"
comd = "startup saved-configuration %s main" % dest
try:
result = comware.CLI(comd, False)
if result == None:
write2Log("\nstartup saved-configuration %s failed" % dest)
print "\n####startup saved-configuration %s failed####" % dest
return False
except Exception as inst:
write2Log("\nstartup %s exception: %s" % (dest, inst))
print "\n####startup %s exception: %s####" % (dest, inst)
return False
write2Log("\nstartup saved-configuration %s success" % dest)
print "\n###INFO: Completed Startup Saved-configuration###"
return True
#从sn.txt中返回对应的sn号或者irf编号
def getIrfCfg(line, num):
line = line.split()
number = None
if 3 == len(line):
number = line[num]
else :
number = None
return number
#获取设备当前的irf编号
def getMemberID():
aMemId = comware.get_self_slot()
memId = None
if aMemId[0] == -1 :
memId = aMemId[1]
else :
memId = aMemId[0]
return memId
#从sn.txt文件中获取renumber后的编号
def getNewMemberID():
global irf_local_name, local_path, env
filename = "%s%s" %(local_path, irf_local_name)
serNum = os.environ['DEV_SERIAL']
reNum = None
try:
file = open(filename, "r")
line = file.readline()
while "" != line:
if (serNum == getIrfCfg(line, 0)):
file.close()
reNum = getIrfCfg(line, 2)
return reNum
line = file.readline()
file.close()
except Exception as inst:
write2Log("\nget renumberID exception: %s" % inst)
print "\n####get renumberID exception: %s####" % inst
write2Log("\nget %s renumberID failed" % filename)
print "\n#### get %s renumberID failed ####" % filename
return reNum
#确认设备已开启irf堆叠模式
def isIrfDevice():
try:
result = comware.CLI("display irf", False)
if result == None:
return False
except Exception as inst:
return False
return True
#获取将要进行修改irf-member的配置
def getIrfComd():
comd = None
if irf_server_name != "" and irf == True:
newMemberID = getNewMemberID()
else:
newMemberID = 1
aMemId = comware.get_self_slot()
if None == newMemberID:
return None
if False == isIrfDevice():
comd = "system-view ; irf member %s ; chassis convert mode irf" % newMemberID
else:
comd = "system-view ; irf member %s renumber %s" % (getMemberID(), newMemberID)
return comd
#确认设备sn号可以获取,又是需要堆叠的情况,开始进行堆叠,下发renumber的配置
def stackIrfCfg():
global env
if irf_server_name == "" or irf == False:
print "\n###irf_server_name is None,No need irf###"
return True
if (not os.environ.has_key('DEV_SERIAL')):
write2Log("\nenviron variable 'DEV_SERIAL' is not found!")
print "\n####environ variable 'DEV_SERIAL' is not found!####"
return False
comd = getIrfComd()
if None == comd:
return False
result = None
write2Log("\nstartup stack irf begin")
print "\n###INFO: Startup stack irf Start###"
try:
result = comware.CLI(comd, False)
if result == None:
write2Log("\nstartup stack irf failed: %s" % comd)
print "\n####startup stack irf failed: %s####" %comd
return False
except Exception as inst:
write2Log("\nstartup stack irf exception: %s command: %s" % (inst, comd))
print "\n####startup stack irf exception: %s command: %s####" % (inst, comd)
return False
write2Log("\nstartup stack irf success")
print "\n###INFO: Completed Startup Stack Irf###"
return True
#这里感觉配置有点多余,当获取到设备上的member id不是1的时候转为1,当配置中有chassis时需要undo chassis convert mode没太理解
def renumber_id():
MemberID = getMemberID()
comd = getIrfComd()
if "chassis" in comd:
comd = "system-view ; undo chassis convert mode"
elif "renumber" in comd:
if MemberID == 1:
return True
else:
comd = "system-view ; irf member %s renumber 1" % MemberID
else:
return False
try:
result = comware.CLI(comd, False)
if result == None:
write2Log("\nrenumeber id failed: %s" % comd)
print "\n####renumeber id failed: %s####" %comd
return False
except Exception as inst:
write2Log("\nrenumeber id failed exception: %s command: %s" % (inst, comd))
print "\n####renumeber id failed: %s command: %s####" % (inst, comd)
return False
write2Log("\nrenumeber id success")
print "\n###INFO: Completed renumeber id###"
return True
#检查所有的备用slot状态是否正常
#check if all standby slots are ready
def ifAllStandbyReady():
if (("get_slot_range" in dir(comware)) == False):
return True
aSlotRange = comware.get_slot_range()
bAllReady = True
for i in range(aSlotRange["MinSlot"], aSlotRange["MaxSlot"]):
oSlotInfo = comware.get_slot_info(i)
if (oSlotInfo != None) and (oSlotInfo["Role"] == "Standby") and (oSlotInfo["Status"] == "Fail"):
bAllReady = False
write2Log("\nSlot %s is not ready!" %i)
print "\n####Slot %s is not ready!####" %i
return bAllReady
#如果备用板卡没有ready,则每次等待10秒直到都变成normal状态
#if have any standby slot was not ready sleep for waiting
def waitStandbyReady():
while ifAllStandbyReady() == False:
sleep(10)
#python main
主程序
#when download file user can stop script
waitStandbyReady()
signal.signal(signal.SIGTERM, sigterm_handler)
if (get_device_info() != True):
doExit("error")
if (check_version() == True):
if (True == copy_patch()) and (True == copyIrfStack()) and (True == copyCfgFile()):
signal.signal(signal.SIGTERM, sig_handler_no_exit)
if (True == installPatchImage()) and (True == stackIrfCfg()) and (True == startupCfg()):
doExit("success")
elif (True == verifyAllFreeSpace()) and (True == copyBootImage()):
signal.signal(signal.SIGTERM, sig_handler_no_exit)
if True == installBootImage():
doExit("success")
doExit("error")
test1(failed):
test2(problem):
更多推荐
已为社区贡献2条内容
所有评论(0)