python網絡設備配置備份

通過幾天折騰,終於搞出來了一個python網絡設備配置備份的腳本。
基本思路以下:
1.定義設備類Device
1.1初始化參數dc, zone, hostname, ip, user, passwd(數據中心,網絡區域,設備名稱,ip,用戶名,密碼)
1.2函數login設備登陸
1.3函數screenCMD設備測試是否支持分屏命令(目前只有華爲的作這個測試)
1.4函數get_config支持分屏命令的設備取配置方法
1.5函數get_config_more不支持分屏的設備取配置方法
2.判斷設備廠家返回設備函數DeviceVendor
3.定義各個設備的命令函數
4.定義讀寫文件函數FileOps
5.定義時間函數TimeOps
6.主函數,循環讀取設備列表,根據設備廠家執行函數與記錄打印日誌,存檔配置文件python

有優化建議請大神提一提哈!!!shell

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
使用說明:
1.設備列表爲csv格式(文本),第一行爲註釋,執行時會自動跳過第一行
數據中心,網絡分區,設備名稱,設備IP,用戶名,密碼,廠商
2.主函數中的參數即爲該設備列表的名稱
3.日誌:3.1實時打印日誌
        3.2詳細日誌和結果日誌會生成在腳本目錄log
4.配置備份目錄,自動生成,目錄爲YMD網絡配置備份,YMD分別爲年月日,二三級目錄根據數據中心,網絡分區生產

當前版本1.3
版本更新說明
初始版本:支持華爲display cu,測試screen-length 0 temporary
1.0更新,將各廠商設備方法get_config和get_config_more轉成Device類的通用方法
1.1更新:1.locale在不一樣的操做系統可能報錯,時間顯示修改爲默認,
        2.各提示修改成英文,讀寫編碼修改成GBK,解決某些設備寫入錯誤問題。
        3.解決paramiko recv 65535問題,經過while語句,config+=outbuf.decode
        4.寫入配置前替換不支持分屏命令的設備記錄日誌時有 ---- More ----                                           字符
        5.增長了異常拋出登陸時ssh版本問題Incompatible version
        5.增長異常拋出,設備型號不支持記錄並繼續下一步class ErrorType(Device)
1.1.1更新:提示爲中文
1.2更新:1.配置記錄不全的設備嘗試從新登陸備份
1.3更新:1.增長執行詳細記錄log和執行結果log
        2.增長詳細日誌和結果日誌裏的password顯示爲******
        3.將devicelist獨立爲一個參數,放到main函數上
        4.4.F5不分屏modify cli preference pager disabled display-threshold 0
'''
import socket, os, time, re
import paramiko
#定義設備類
class Device(object):
    # 初始化參數,與設備devicelist的列參數一一對應
    #定義通用方法,login登陸,screenCMD判斷是否支持分屏命令,
    # get_config通用分屏命令取配置,get_config_more爲不支持分屏命令的取配置方法
    def __init__(self, dc, zone, hostname, ip, user, passwd):
        self.dc=dc
        self.zone=zone
        self.hostname = hostname
        self.ip = ip
        self.user = user
        self.passwd = passwd
        self.success_flag = True
        self.screen_cmd_flag = True
        self.success_login_count = 0#登陸成功次數
        self.success_backup_count = 0#備份成功次數
        self.fail_backup_count = 0#備份失敗次數
        self.fail_login_count = 0#登錄失敗次數
        self.fail_backup_context = ''#存放備份失敗的設備信息
        self.fail_login_context = ''#存放登陸失敗的設備信息
        self.exe_detail_log=''# 保存執行過程詳細日誌
        self.ssh= paramiko.SSHClient()
        self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    #登陸
    def login(self):
        if self.success_flag:
            try:
                self.ssh.connect(hostname=self.ip, username=self.user, password=self.passwd, look_for_keys=False, timeout=5)
                self.success_login_count = 1
            except paramiko.ssh_exception.AuthenticationException as e:
                self.success_flag = False
                self.fail_login_count = 1
                self.fail_login_context='%s-%s - %s' % (self.hostname,self.ip, e)
                self.exe_detail_log+=(TimeOps.get_time_stamp()+','+self.fail_login_context+'\n')
                print(self.fail_login_context)
            except paramiko.ssh_exception.SSHException as e:
                #paramiko.ssh_exception.SSHException: Incompatible version (1.5 instead of 2.0)
                self.success_flag = False
                self.fail_login_count = 1
                self.fail_login_context='%s-%s - %s' % (self.hostname,self.ip, e)
                self.exe_detail_log+=(TimeOps.get_time_stamp()+','+self.fail_login_context+'\n')
                print(self.fail_login_context)
            except socket.timeout as e:
                self.success_flag = False
                self.fail_login_count = 1
                self.fail_login_context='%s-%s - %s' % (self.hostname,self.ip, e)
                self.exe_detail_log+=(TimeOps.get_time_stamp()+','+self.fail_login_context+'\n')
                print(self.fail_login_context)
            else:
                pass
    #判斷是否支持分屏
    def screenCMD(self,scmd):
        Device.login(self)
        if self.success_flag :
            cmd = self.ssh.invoke_shell()
            cmd.send(scmd+'\n')
            time.sleep(1)
            out_buf = cmd.recv(65535).decode('gbk', 'ignore')
            self.ssh.close()
            if re.findall(r'Error:|Unrecognized command|Incomplete command|Wrong parameter', out_buf):
                self.screen_cmd_flag = False
                self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + '%s-%s設備不支持分屏disable命令'%(self.hostname,self.ip)+'\n')
    #支持分屏的獲取命令,並將日誌寫入爲文件
    def get_config(self,log_return,*args):#log_return爲配置完成的返回值(好比return),*args爲命令集list
        recv_config=''#str類型
        start_time=time.time()
        Device.login(self)#登陸設備
        if self.success_flag :
            success_log='%s-%s 登陸成功。'%(self.hostname,self.ip)
            print(success_log,end='')
            self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + success_log+'\n')
            cmd=self.ssh.invoke_shell()
            for i in args:  # 循環讀取命令集,並逐一執行
                time.sleep(0.3)
                cmd.send(i + "\n")
            time.sleep(3)  # 測試3s能完整記錄,有些設備登陸比較慢(H3C),在下一環節進行時間等待if 'return' not in str(out_buf)
            while True:  # 持續接受通道的數據流,解決recv 65535問題
                if cmd.recv_ready():
                    out_buf = cmd.recv(65535)  # byte類型
                    if len(out_buf) == 0:
                        raise EOFError('通道數據流被遠程設備關閉。')
                    recv_config += out_buf.decode('gbk', 'ignore')
                    if log_return not in str(out_buf):#若是記錄未完成,繼續等待記錄
                        time.sleep(0.3)
                else:
                    break
            if log_return in recv_config:
                self.success_backup_count = 1
                success_log = '日誌數據流記錄成功。'
                print(success_log, end='')
                self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + success_log+'\n')
            else:
                self.fail_backup_count = 1
                self.fail_backup_context='%s日誌中沒有包含關鍵字 %s 。'%(self.ip,log_return)
                self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + self.fail_backup_context+'\n')
                print('%s日誌中沒有包含關鍵字 \033[0;31m%s\033[0m 。'%(self.ip,log_return),end='')
            self.ssh.close()
            recv_config = recv_config.replace('\r\n', '\n')  # Windows顯示\r\n分行替換爲\n
            FileOps.writecfg(self.dc, self.zone, self.hostname, recv_config)#將日誌寫到文件
            end_time=time.time()
            time_log='耗時 %s 秒。'%int(end_time-start_time)
            self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + time_log+'\n')
            print(time_log)

    # 不支持分屏的獲取命令,並將日誌寫入爲文件
    def get_config_more(self,log_return,*args):#接受命令集
        recv_config=''#str類型
        start_time=time.time()
        Device.login(self)#設備登陸
        if self.success_flag :
            success_log='%s-%s 登陸成功。'%(self.hostname,self.ip)
            print(success_log,end='')
            self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + success_log+'\n')
            cmd=self.ssh.invoke_shell()
            for i in args:#循環讀取命令集,並逐一執行
                time.sleep(0.3)
                cmd.send(i+"\n")
            while True:#持續接受通道的數據流,解決recv 65535問題
                if cmd.recv_ready():
                    out_buf=cmd.recv(65535)#byte類型
                    if len(out_buf)==0:
                        raise EOFError('通道數據流被遠程設備關閉。')
                    recv_config +=out_buf.decode('gbk', 'ignore')
                if log_return not in str(out_buf) and str(out_buf).endswith('--')==False:#可能有多種狀況的more--,取--
                    cmd.send(' ')
                    time.sleep(0.1)
                else:
                    break
            if log_return in recv_config:
                self.success_backup_count = 1
                success_log = '日誌數據流記錄成功。'
                print(success_log, end='')
                self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + success_log+'\n')
            else:
                self.fail_backup_count = 1
                self.fail_backup_context='%s日誌中沒有包含關鍵字 %s 。'%(self.ip,log_return)
                self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + self.fail_backup_context+'\n')
                print('%s日誌中沒有包含關鍵字 \033[0;31m%s\033[0m 。'%(self.ip,log_return),end='')
            self.ssh.close()
            recv_config = recv_config.replace('\r\n','\n')#Windows顯示\r\n分行替換爲\n
            self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + '執行替換\\r\\n爲\\n動做。'+'\n')
            recv_config = recv_config.replace('  ---- More ----                                          ','')
            recv_config = recv_config.replace(' ---- More ----                                           ','')
            self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + '執行替換---- More ----動做。'+'\n')
            #解決More的顯示問題
            FileOps.writecfg(self.dc, self.zone, self.hostname, recv_config)
            end_time=time.time()
            time_log='耗時 %s 秒。'%int(end_time-start_time)
            self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + time_log+'\n')
            print(time_log)
#華爲
class Huawei(Device):
    #華爲須要判斷是否支持分屏
    def ops(self):
        #測試是否支持分屏命令
        Huawei.screenCMD(self,'screen-length 0 temporary')
        if self.success_flag:
            if self.screen_cmd_flag:
                Huawei.get_config(self,'return','screen-length 0 temporary','display cu')
                # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
            else:
                Huawei.get_config_more(self,'return','display cu')
                # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
class H3C(Device):
    def ops(self):
        if self.success_flag:
            H3C.get_config(self,'return','screen-length disable','display cu')
            # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
class Cisco(Device):
    def ops(self):
        if self.success_flag:
            Cisco.get_config(self,'line vty','terminal length 0','show run')
            # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
class ZTE(Device):
    def ops(self):
        if self.success_flag:
            ZTE.get_config(self,'end','terminal length 0','show run')
            # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
class A10(Device):
    def ops(self):
        if self.success_flag:
            A10.get_config(self,'end','enable\n','terminal length 0','show running-config all-partitions')
            # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
class FiberHome(Device):
    def ops(self):
        if self.success_flag:
            FiberHome.get_config(self,'#','enable','terminal length 0','show running-config')
            # 第一個爲記錄配置完成標誌的返回值,後面爲命令集
class F5(Device):
    def ops(self):
        if self.success_flag:
            F5.get_config(self,'(tmos)#','tmsh','modify cli preference pager disabled display-threshold 0','show running-config')
class TOPSEC(Device):
    def ops(self):
        if self.success_flag:
            TOPSEC.get_config(self,'config implement','show-running nostop')
class Forti(Device):
    def ops(self):
        if self.success_flag:
            Forti.get_config(self,'router setting','config system console','set output standard','end','show full-configuration')
class Maipu(Device):
    def ops(self):
        if self.success_flag:
            Maipu.get_config(self,'end','more off','show running-config')
class ErrorType(Device):
    def ops(self):
        self.fail_login_count = 1
        self.fail_login_context = '不支持 %s-%s 該設備型號。' % (self.hostname, self.ip)
        self.exe_detail_log += (TimeOps.get_time_stamp() + ',' + self.fail_login_context+'\n')
        print('不支持 \033[0;31m%s-%s\033[0m 該設備型號。' % (self.hostname, self.ip))

#定義文件操做,包括讀取設備列表文件,設備配置存檔
class FileOps(object):
    #文件讀取
    cur_path = os.getcwd()
    write_main_path = cur_path + ('\\%s網絡配置備份' % time.strftime('%Y%m%d'))#備份的主目錄
    @staticmethod
    def readcfg(file):
        with open(file, 'r', encoding='GBK') as f:
            filelist = f.read().split('\n')
            filelist.pop(0)#配置文件首行爲註釋行
        return filelist
    # 文件保存
    @staticmethod
    def writecfg(dc,zone,hostname,cfg):
        dev_path='\\%s\\%s'%(dc,zone)#設備子目錄,根據設備數據中心/網絡分區劃分
        write_path=FileOps.write_main_path+dev_path#寫入的詳細目錄
        if not os.path.exists(write_path):
            os.makedirs(write_path)
        file='%s.txt'%(hostname)
        with open(write_path+'\\'+file, 'w',encoding='gbk') as f:
            f.write(cfg)
            print('配置備份成功,保存爲 %s\%s' % (dev_path,file),end='。')#只顯示設備子目錄
    #日誌寫入
    @staticmethod
    def writelog(logstr,logname):
        logpath='log'
        log_separator='分 隔 行'.center(60,'-')
        if not os.path.exists(logpath):
            os.makedirs(logpath)
        logname=logname+'%s.log'%time.strftime('%Y%m%d')
        with open(logpath+'\\'+logname, 'a',encoding='gbk') as f:
            f.write('\n'+log_separator+'\n'+logstr)
#設備廠商判斷,返回設備廠商函數,自動實例化
class DeviceVendor(object):
    # 判斷設備廠商,自動實例化
    @staticmethod
    def OPS(*args):#任意列表參數
        if str(args[6]).upper() == 'HUAWEI':
            return Huawei(*args[0:-1])
        elif str(args[6]).upper() == 'H3C':
            return H3C(*args[0:-1])
        elif str(args[6]).upper() == 'CISCO':
            return Cisco(*args[0:-1])
        elif str(args[6]).upper() == 'ZTE':
            return ZTE(*args[0:-1])
        elif str(args[6]).upper() == 'A10':
            return A10(*args[0:-1])
        elif str(args[6]).upper() == 'FIBERHOME':
            return FiberHome(*args[0:-1])
        elif str(args[6]).upper() == 'F5':
            return F5(*args[0:-1])
        elif str(args[6]).upper() == 'TOPSEC':
            return TOPSEC(*args[0:-1])
        elif str(args[6]).upper() == 'FORTI':
            return Forti(*args[0:-1])
        elif str(args[6]).upper() == 'MAIPU':
            return Maipu(*args[0:-1])
        else:
            return ErrorType(*args[0:-1])#不支持的設備類型
class TimeOps(object):#計算時間戳到毫秒
    @staticmethod
    def get_time_stamp():
        ct = time.time()
        local_time = time.localtime(ct)
        data_head = time.strftime("%Y-%m-%d %H:%M:%S", local_time)
        data_secs = (ct - int(ct)) * 1000
        time_stamp = "%s.%03d" % (data_head, data_secs)
        return time_stamp
def main(devicelist):
    all_exe_detail_log=''#運行詳細日誌
    all_exe_result_log=''#運行結果日誌
    start_time = time.time()
    start_log='%s,配置備份程序正在運行,請等待!!!'%time.strftime('%Y/%m/%d %H:%M:%S')+\
              '\n'+'備份路徑爲 : %s'%FileOps.write_main_path
    all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + start_log+'\n')
    print(start_log)
    devicelist=FileOps.readcfg(devicelist)#讀取設備列表,這裏已轉爲list[]
    all_success_login_count = 0  # 登陸成功次數
    all_success_backup_count = 0  # 備份成功次數
    all_fail_backup_count = 0  # 備份失敗次數
    all_fail_login_count = 0  # 登錄失敗次數
    devicelistErr_count=0
    all_fail_backup_context = []  # 存放備份失敗的設備信息
    all_fail_login_context = []  # 存放登陸失敗的設備信息
    devicelistErr_context=[]    # 存放設備list參數錯誤的設備信息
    all_fail_backup_dev_dict={} # 存放設備備份記錄不全的設備列表,用於下次嘗試繼續登陸備份
    devicenum=0#統計總的設備數量
    for i in devicelist:#循環設備列表[]的每行
        list=i.split(',')#將每行的str轉換爲list[]
        if len(list)==7:#斷定該行的參數是否完整,可能出現空行,須要跳過
            devicenum+=1
            DeviceOps=DeviceVendor().OPS(*list)#判斷設備廠商,執行設備廠商class,參數以list傳入
            DeviceOps.ops()#調用廠商方法
            all_success_login_count+=DeviceOps.success_login_count#記錄登陸成功的次數
            all_success_backup_count+=DeviceOps.success_backup_count#記錄備份成功的次數
            all_fail_login_count+=DeviceOps.fail_login_count#記錄登陸失敗的次數
            all_fail_backup_count+=DeviceOps.fail_backup_count#記錄備份失敗的次數
            if DeviceOps.fail_login_context != '':
                all_fail_login_context.append(DeviceOps.fail_login_context)#記錄登陸失敗的日誌
            if DeviceOps.fail_backup_context != '':
                all_fail_backup_context.append(DeviceOps.fail_backup_context)#記錄備份失敗的日誌
                fail_backup_index=all_fail_backup_context.index(DeviceOps.fail_backup_context)#該設備失敗日誌在總的失敗日誌裏的index
                all_fail_backup_dev_dict[fail_backup_index]=i
                #將設備信息加入到備份失敗字典,用於下次繼續嘗試登陸備份,key爲敗日誌裏的index(方便後續刪除),value爲設備參數信息
            all_exe_detail_log += DeviceOps.exe_detail_log  # 記錄運行詳細日誌
        else:#不然記錄錯誤設備參數並打印
            devicenum+=1
            devicelistErr_count+=1
            if len(list)>5:
                list[5]='******'#將密碼替換成******
            devicelistErr='Devicelist第 %s 行參數不對或者空行,行信息以下: %s'%(devicelist.index(i)+2,','.join(list))
            #devicelist.index(i)+2,list從0開始,且pop了第一行註釋,所以須要+2
            devicelistErr_context.append(devicelistErr)
            all_exe_detail_log += (TimeOps.get_time_stamp()+','+devicelistErr)  # 記錄運行詳細日誌
            print('Devicelist第 \033[0;31m%s\033[0m 行參數不對或者空行,行信息以下: %s'%(devicelist.index(i)+2,','.join(list)))
    #嘗試從新登陸備份
    if all_fail_backup_dev_dict!={}:
        re_backup_log='\n嘗試從新登陸備份失敗的設備!'
        print('\n\033[1m嘗試從新登陸備份失敗的設備!\033[0m')
        all_exe_detail_log += (TimeOps.get_time_stamp()+','+re_backup_log+'\n')  # 記錄運行詳細日誌
        for k,v in all_fail_backup_dev_dict.items():  # 遍歷字典,k爲all_fail_backup_context的index,v爲設備參數
            list = v.split(',')  # 將每行的str轉換爲list[]
            DeviceOps = DeviceVendor().OPS(*list)  # 判斷設備廠商,執行設備廠商class,參數以list傳入
            DeviceOps.ops()  # 調用廠商方法
            if DeviceOps.fail_backup_context != '':#備份失敗
                re_backup_failed_log='從新備份失敗,%s'%DeviceOps.fail_backup_context
                all_exe_detail_log += (TimeOps.get_time_stamp() +','+ re_backup_failed_log+'\n')
                print(re_backup_failed_log)
            else:
                all_success_backup_count += DeviceOps.success_backup_count  # 增長備份成功的次數
                all_fail_backup_count -= DeviceOps.success_backup_count  # 減小備份失敗的次數減
                all_fail_backup_context.pop(k)#刪除備份失敗的內容
                re_backup_success_log='%s-%s從新備份成功'%(DeviceOps.hostname,DeviceOps.ip)
                all_exe_detail_log += (TimeOps.get_time_stamp() +','+ re_backup_success_log)
                print(re_backup_success_log)

    #結果信息
    result_log='\n#*#*#*#*備 份 結 果*#*#*#*#'
    all_exe_detail_log+=(TimeOps.get_time_stamp()+','+result_log+'\n')
    all_exe_result_log+=(TimeOps.get_time_stamp()+'\n'+result_log+'\n')
    print('\n\033[1m#*#*#*#*備 份 結 果*#*#*#*#\033[0m')
    dev_log='設備總數 %s ,成功備份的設備數量 %s .'%(devicenum,all_success_backup_count)
    all_exe_detail_log+=(TimeOps.get_time_stamp()+','+dev_log+'\n')
    all_exe_result_log+=(dev_log+'\n')
    print('設備總數 \033[1m%s \033[0m,成功備份的設備數量 \033[1m%s \033[0m.'%(devicenum,all_success_backup_count))
    if all_fail_backup_count > 0 :#備份失敗設備信息
        all_fail_backup_log='\n#備份失敗的設備數量: %s ,具體以下: '%all_fail_backup_count
        all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + all_fail_backup_log+'\n')
        all_exe_result_log += (all_fail_backup_log+'\n')
        print('\n#\033[1;31m備份失敗\033[0m的設備數量: \033[1;31m%s\033[0m ,具體以下: '%all_fail_backup_count)
        for i in all_fail_backup_context:
            print('\t',i)
            all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + '\t'+i+'\n')
            all_exe_result_log += ('\t'+i+'\n')
    if all_fail_login_count > 0 :#登陸失敗設備信息
        all_fail_login_log='登陸失敗的設備數量: %s ,具體以下: '%all_fail_login_count
        all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + all_fail_login_log+'\n')
        all_exe_result_log += (all_fail_login_log+'\n')
        print('\033[1;31m登陸失敗\033[0m的設備數量: \033[1;31m%s\033[0m ,具體以下: '%all_fail_login_count)
        for i in all_fail_login_context:
            print('\t',i)
            all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + '\t'+i+'\n')
            all_exe_result_log += ('\t'+i+'\n')
    if devicelistErr_count > 0 :#設備列表參數錯誤
        dev_err_log='在devicelst.csv文件中,有 %s 行參數錯誤,具體以下:'%devicelistErr_count
        all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + dev_err_log+'\n')
        all_exe_result_log += (dev_err_log+'\n')
        print('在devicelst.csv文件中,有 \033[1;31m%s 行參數錯誤\033[0m,具體以下:'%devicelistErr_count)
        for i in devicelistErr_context:
            list=i.split(',')#將每行的str轉換爲list[]
            if len(list) >= 6:#由於devicelistErr_context裏多了Devicelist第 x 行參數不對或者空行', '行信息以下:,裏面有一個‘,’,所以+1
                list[6]='******'#將密碼加密
            i=','.join(list)#從新轉換成str
            print('\t',i)
            all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + '\t'+i+'\n')
            all_exe_result_log += ('\t'+i+'\n')
    end_time = time.time()
    end_log='\n程序運行總耗時: %s 秒。'%(int(end_time - start_time))
    all_exe_detail_log += (TimeOps.get_time_stamp() + ',' + end_log+'\n')
    all_exe_result_log += (end_log+'\n')
    FileOps.writelog(all_exe_detail_log,'detail')
    FileOps.writelog(all_exe_result_log, 'result')
    print(end_log)
if __name__ == "__main__":
    main('devicelist.csv')
    #主函數,參數爲設備列表名稱
相關文章
相關標籤/搜索