背景需求
日常网络运维,为了审计和防止设备故障导致设备配置丢失而造成长时间网络故障,需要对网络设备的配置做策略备份。
开源软件
oxidized - https://github.com/ytti/oxidized
Bacula - https://www.bacula.org/source-download-center
开源软件场景:跨平台,支持多厂商系统,UI界面友好,功能强大,备份策略可自定义。
关于oxidized的搭建过程,可参考:
瞿同学(Darren):网络设备配置备份-Oxidized8 赞同 · 4 评论文章
自写脚本
- Python脚本
- Go脚本
自写脚本场景:自定义强,适应度高,可根据自身的需求不断优化迭代。
Python脚本备份
需求说明:
公司内部企业网交换机和防火墙以及其他配套设备需要做配置备份,需要满足以下几点需求:
- 支持华为、思科、华三、锐捷等主流厂商交换机系统
- 支持密码加密和解密
- 支持成功备份邮件通知
- 支持TFTP和FTP登录上传备份文件
- 支持可自定义备份策略
思路拓展:
1、首先是如何备份思考?
- 无外乎就是执行查看全局配置的命令,然后抓取回显,保存成配置文件。
- TFTP/FTP登录网元,拷贝配置文件,或者反过来网元作为客户端,FTP/TFTP搭建在Linux/Windows主机中。
- 现在很多厂商新设备都是支持自动备份配置命令,这也是一种手段。
2、多厂商交换机系统支持,如果采用FTP就不需要考虑太多,因为基本上每家厂商都支持FTP。
3、每台设备的密码都不尽相同,需要针对密码做加密和解密操作,执行命令的时候不直接显示明文密码。
4、备份成功后,需通过阿里云邮箱发送邮件通知。
5、自定义备份策略,需要单独开启模块。
开源参考:
GitHub - BarryCui/NetDevOps: 网络设备配置备份。可以备份cisco ios交换机,华为quidway交换机以及fortigate防火墙的配置。
蜗牛勇士/网络设备自动备份。
Feiinbeer:python脚本自动备份网络设备配置文件
自我实现:
1、前期完成基础的需求,并正式运用在测试环境中进行测试,并不断改善,后续应用在生产环境中。
2、中期思考如何结合Web UI进行监控和操作。
3、后期可以考虑形成一个比较完善的网络设备备份系统平台,进行总体闭环,并开源到Github和Gitee。
程序实例
网络设备配置备份程序 摘要
- 网络设备的自动化实现方式:SNMP/NetConf + NAPALM/RestConf/OpenConfig
- 本程序是通过最原始的ssh协议来备份网络设备的配置。
- 使用场景为:不支持API的网络设备或运维人员不会调用API的场景。
- 备份说明:备份方式是通过ssh协议结合ftp协议进行,读取网络配置文件,通过paramiko模块连接网络设备发送put命令,把配置文件上传FTP服务器,完成网络设备配置文件的备份。
- 目前支持备份:目前只支持华为、华三系列设备,后续将进行适配支持更多其他厂商设备。
程序结构说明
当前目录依赖包生成:pip install pipreqspipreqs . --encoding=utf8 --force
目录结构
- AutoBakCfg.yml 是存放FTP服务器配置信息(登录用户、登录密码、FTP服务器IP、网络设备明文密码加密开关)
- requirements.txt 当前程序python依赖库
- dev_info.xlsx 网络设备信息(登录用户、登录密码、设备管理IP、配置文件路径)
- password_aes.py 对明文密码进行AES加密和解密处理
- configuration_rw.py 对YAML配置文件的读写处理
- write_excels.py 对xlsx文本数据的读写处理
- ftp_server.py 模拟FTP客户端进行FTP服务器验证
- utils.py 本程序依赖的一些工具函数
- run_backup.py 程序主入口
环境说明
- Python 3.11.0
- Windows 10、11系统
- Pycharm 2022.1.2专业版
Linux发行版暂未测试,后续发出Linux版本上的运行环境说明。
网络设备支持
- HUAWEI
- H3C
后续进行优化迭代,加入思科、锐捷和其他主流厂商的支持。
FTP服务器
- Universal FTP Server(Microsoft Store可直接下载)

Universal FTP Server
程序运行结果说明
- 程序会自动把网络设备配置文件备份到服务器的根目录下
代码示例

示例
AutoBakCfg.yml 测试文件:FTP服务器信息和加密开关参数
base-info: ftp_password: '123456' ftp_server: 10.10.10.2 ftp_user: python is_encrypt: '1'
requirements.txt 项目依赖库
pandas==1.5.3paramiko==3.0.0pycryptodome==3.17PyYAML==6.0
dev_info.xlsx 网络设备信息

设备信息
password_aes.py
#!/usr/bin/env python3# -*- coding: utf-8 -*-# Linux下和Windows下安装pycryptodome 把读取的xlsx文档的密码进行加密和解密处理# pip install pycryptodomefrom Crypto.Cipher import AESfrom Crypto.Util.Padding import padfrom Crypto.Util.Padding import unpadfrom binascii import b2a_hex, a2b_heximport reclass CryptPassword: """ AES.new 初始化AES对象 """ def __init__(self, key, init_vector): """ :param key: 初始化AES加密密钥 AES.new传入参数必须是bytes类型 :param init_vector: 初始化iv偏移量 AES.new传入参数必须是bytes类型 """ self.key = key self.init_vector = init_vector self.mode = AES.MODE_CBC # self.cryptor = AES.new(self.key, self.mode, self.init_vector) # AES加密 待加密的字符串为str类型并返回str类型数据 def encrypt_str(self, pass_str) -> str: # encode('ascii') 字符串转字节串,把字符串:abc,转为字节串:b'abc' cryptor = AES.new(self.key.encode('ascii'), self.mode, self.init_vector.encode('ascii')) # pass_str str类型 pad的data_to_pad必须为bytes类型 故pass_str需要转换为字节串 padtext = pad(pass_str.encode('ascii'), 16, style='pkcs7') # 调用加密方法 cipher_text = cryptor.encrypt(padtext) # 字符串 --> 十六进制 str_cipher = b2a_hex(cipher_text) return str_cipher.decode('ascii') # AES解密 待解密的字符串为str类型并返回str类型数据 def decode_str(self, text_str) -> str: if not CryptPassword.is_hex(text_str) or (len(text_str) < 32): return "错误的16进制加密字符串" else: # 十六进制 --> 字符串 cipher_text = a2b_hex(text_str) decrypter = AES.new(self.key.encode('ascii'), self.mode, self.init_vector.encode('ascii')) # 调用解密方法 plain_text = decrypter.decrypt(cipher_text) # 不够16位,补全到16位 try: unpadtext = unpad(plain_text, 16, 'pkcs7') # decode('ascii') 字节串转字符串,把字节串:b'abc',转为字符串:abc return unpadtext.decode('ascii') except ValueError: return "请输入正确的加密字符串" # 16进制检查 @staticmethod def is_hex(hex_str) -> bool: # 16进制的正则表达式 regex = "[A-Fa-f0-9]+$" result = re.match(regex, hex_str) if result: return True else: return False# 测试用例if __name__ == '__main__': key_word = 'QWErtyuio@098765' init_vector_word = 'MEIYuanTian--Xia' password = 'T5fx2L3aFt' data = CryptPassword(key_word, init_vector_word) print(data.encrypt_str(password)) print(CryptPassword.is_hex(data.encrypt_str(password))) text = 'c0cf24e87b328810c0b5711b2f326c8f' print(data.decode_str(text))
configuration_rw.py
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""针对yaml/yml等配置文件进行读取和写入的实现类"""import yamlimport osimport pathlibclass YamlReadWrite: # 初始化 def __init__(self, filepath): self.filepath = filepath def yaml_read(self, file_name): # filename = str(file_name) # print(filename) if os.path.isfile(file_name): with open(self.filepath + '\\' + file_name, 'r', encoding='utf-8') as fp: line_data = yaml.load(fp, Loader=yaml.FullLoader) return line_data else: print("配置文件不存在") def yaml_write(self, file_name, encrypt_str): # 读取yaml文件数据 if os.path.isfile(file_name): old_file_data = YamlReadWrite(self.filepath).yaml_read(file_name) # 修改读取数据 old_file_data['base-info']['is_encrypt'] = encrypt_str with open(self.filepath + '\\' + file_name, 'w', encoding='utf-8') as fp: yaml.dump(old_file_data, fp) return "密码已加密,配置文件加密参数修改为0" else: print("配置文件不存在")if __name__ == '__main__': path = os.path.dirname(__file__) read_data = YamlReadWrite(path).yaml_read(file_name='AutoBakCfg.yml') print(read_data['base-info']['ftp_password']) write_data = YamlReadWrite(path).yaml_write(file_name='AutoBakCfg.yml', encrypt_str='0') print(write_data) print("修改完成")
write_excels.py
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""利用pandas模块读写excel文本数据,并对文本数据中的密码进行判断,如果不是16进制字符串,那就进行加密并重新写入,读取16进制字符串解密进行ftp登录依赖模块: pandas,openpyxl"""import pandas as pdfrom password_aes import CryptPasswordclass MachiningText: def __init__(self, xlsx_file_name): self.xlsx_file_name = xlsx_file_name # 更新格式化后的密码并写入xlsx文件中 def write_xlsx_file(self, dev_name, dev_ip, dev_user, dev_pw, file_path): data_df = pd.DataFrame() data_df["设备名称"] = dev_name data_df["设备IP"] = dev_ip data_df["账号"] = dev_user data_df["密码"] = dev_pw data_df["文件路径"] = file_path try: writer = pd.ExcelWriter(self.xlsx_file_name) data_df.to_excel(writer, sheet_name='dev_info', index=False) writer.close() except PermissionError: print("请先关闭文档,再次尝试") def update_password(self, key, init_vector): dev_name = [] dev_ip = [] dev_user = [] dev_pw = [] file_path = [] dev_pd = pd.read_excel(self.xlsx_file_name, sheet_name='dev_info') # 赋予i=0 密码已加密,i=1 把明文密码转为加密密码 i = 0 for line_str in dev_pd.values: dev_name = dev_name + [line_str[0].strip()] dev_ip = dev_ip + [line_str[1].strip()] dev_user = dev_user + [line_str[2].strip()] # try: # dev_pw = dev_pw + [line_str[3].strip()] # except AttributeError: # dev_pw = dev_pw + [line_str[3]] file_path = file_path + [line_str[4].strip()] # 如果密码是单纯的数字 len(123)会出错,需要转换为str类型 try: if int(line_str[3]): password_len = len(str(line_str[3])) passwd_str = str(line_str[3]) if not CryptPassword.is_hex(passwd_str) or (password_len < 32): new_password = CryptPassword(key, init_vector).encrypt_str(passwd_str) # print(new_password) # print(dev_pw) dev_pw = dev_pw + [new_password] i = 1 elif str(line_str[3].strip()): password_len = len(line_str[3].strip()) if not CryptPassword.is_hex(line_str[3].strip()) or (password_len < 32): new_password = CryptPassword(key, init_vector).encrypt_str(line_str[3].encode('ascii')) dev_pw = dev_pw + [new_password] i = 1 else: return "不存在的类型" except ValueError: # 密码不是16进制,则进行加密 password_len = len(line_str[3].strip()) if (not CryptPassword.is_hex(line_str[3].strip())) or (password_len < 32): new_password = CryptPassword(key, init_vector).encrypt_str(line_str[3].strip()) dev_pw = dev_pw + [new_password] i = 1 else: new_password = [line_str[3]] # 如果密码是16进制,new_password 取表中密码,赋予Dev_pw dev_pw = dev_pw + new_password # 密码有加密,则重新写xls表,否则不需要重写 # print(dev_ip) # print(dev_user) # print(dev_pw) # print(file_path) if i == 1: MachiningText(self.xlsx_file_name).write_xlsx_file(dev_name, dev_ip, dev_user, dev_pw, file_path)if __name__ == '__main__': key_word = 'QWErtyuio@098765' init_vector_word = 'MEIYuanTian--Xia' MachiningText(r'./dev_info.xlsx').update_password(key_word, init_vector_word) print("完成")
ftp_server.py
#!/usr/bin/env python3# -*- coding: utf-8 -*-# 模拟客户端进行FTP服务器验证import ftplibimport utilsclass FTPServer: def __init__(self, ftp_server): self.ftp_server = ftp_server def connect_ftp_server(self, ftp_user, ftp_password) -> bool: print("正在检测FTP服务器,请稍候......") try: ftp = ftplib.FTP(self.ftp_server) print(ftp.getwelcome()) ftp.login(ftp_user, ftp_password) ftp.encoding = 'utf-8' # 230 Logged in 230代表登录成功 login_response = ftp.login(ftp_user, ftp_password) # 通过指定分隔符对字符串进行切片 login_response.split(" ")[0] if int(login_response.split(" ")[0]) == 230: print("FTP服务器登陆成功!") utils.writelog("FTP服务器登陆成功!\n") return True else: return False except ConnectionRefusedError: print("FTP服务器无法登录,请检查服务是否启动!") utils.writelog("FTP服务器无法登录,请检查服务是否启动!\n") return Falseif __name__ == '__main__': user = 'python' password = '123456' server = '10.10.10.2' FTPServer(server).connect_ftp_server(user, password) print("完成")
utils.py