├── LICENSE ├── README.md ├── hosts.yaml ├── images └── switch.png ├── requirements.txt └── switchbackup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 xin053 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 交换机配置自动备份 2 | 3 | 使用 `python3` 多线程 `ssh` 批量登录交换机并获取到交换机配置,自动删除 `30` 天以前的配置,将命令配置到 `crontab` 实现每天备份,支持新华三,华为,锐捷,思科交换机 4 | 5 | ### 安装依赖 6 | 7 | ```shell 8 | # python3 环境 9 | # ubuntu 10 | apt install -y python3-pip 11 | # centos 12 | yum install -y python3-pip 13 | 14 | # pip3 更新并设置源 15 | pip3 install pip --upgrade -i https://mirrors.aliyun.com/pypi/simple/ 16 | pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple/ 17 | 18 | cd /opt 19 | git clone https://github.com/xin053/switchbackup 20 | cd switchbackup 21 | pip3 install -r requirements.txt 22 | ``` 23 | 24 | ### 修改配置文件 `hosts.yaml` 25 | 26 | 按照以下格式, 注意缩进, `yaml` 文件对缩进要求很严格 27 | 28 | 支持的 `type` 有 `h3c, huawei, ruijie, cisco` 29 | 30 | ```yaml 31 | # 备份文件保存路径 32 | backup_path: '/home/xin053/swConfigBackup' 33 | # 备份文件保存时长, 单位: 天 34 | keep_time: 30 35 | hosts: 36 | - name: xxxH3C6800 37 | type: h3c 38 | ip: xxx.xxx.xxx.xxx 39 | port: 22 40 | username: xxx 41 | password: xxx 42 | - name: xxxCE6810-01 43 | type: huawei 44 | ip: xxx.xxx.xxx.xxx 45 | port: 22 46 | username: xxx 47 | password: xxx 48 | ``` 49 | 50 | ### 使用 51 | 52 | ```shell 53 | # 命令格式 54 | python3 switchbackup.py [ip] [ip] ... 55 | 56 | cd /opt/switchbackup 57 | # 备份配置文件中的全部交换机 58 | python3 switchbackup.py 59 | # 备份配置文件中指定交换机 60 | python3 switchbackup.py xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx 61 | ``` 62 | 63 | ### 配置 `crontab` 64 | 65 | 每天凌晨执行备份: 66 | 67 | ```shell 68 | 0 0 * * * cd /opt/switchbackup && python3 switchbackup.py 69 | ``` 70 | 71 | ### 效果图 72 | 73 | ![](./images/switch.png) 74 | -------------------------------------------------------------------------------- /hosts.yaml: -------------------------------------------------------------------------------- 1 | # 备份文件保存路径 2 | backup_path: '/home/xin053/swConfigBackup' 3 | # 备份文件保存时长, 单位: 天 4 | keep_time: 30 5 | hosts: 6 | - name: xxxH3C6800 7 | type: h3c 8 | ip: xxx.xxx.xxx.xxx 9 | port: 22 10 | username: xxx 11 | password: xxx 12 | - name: xxxCE6810-01 13 | type: huawei 14 | ip: xxx.xxx.xxx.xxx 15 | port: 22 16 | username: xxx 17 | password: xxx -------------------------------------------------------------------------------- /images/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xin053/switchbackup/10e38f238e7ce28f06348b8b648e87cba45e96cb/images/switch.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pexpect==4.8.0 2 | ruamel.yaml==0.16.10 3 | -------------------------------------------------------------------------------- /switchbackup.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import datetime 3 | import logging 4 | from multiprocessing import cpu_count 5 | import os 6 | import pathlib 7 | import pexpect 8 | import sys 9 | import time 10 | from ruamel.yaml import YAML 11 | 12 | 13 | # h3c 交换机配置备份 14 | def H3CAutoConfig(host): 15 | ssh = pexpect.spawn('ssh -o StrictHostKeyChecking=no -p %s %s@%s ' % 16 | (host['port'], host['username'], host['ip'])) 17 | 18 | try: 19 | ssh.expect('[Pp]assword:', timeout=30) 20 | ssh.sendline(host['password']) 21 | ssh_info = ssh.expect(['>$', '[Pp]assword:']) 22 | if ssh_info == 0: 23 | ssh.sendline('screen-length disable') 24 | ssh.expect(['>$', ']$']) 25 | ssh.sendline('dis cur') 26 | ssh.expect(['>$', ']$']) 27 | with open(log_filename + '/' + host['ip'] + '.txt', 'w') as f: 28 | print(ssh.before.decode('utf8', 'ignore').replace('\r', ''), 29 | file=f) 30 | ssh.close() 31 | logger.info('backup switch {}[{}:{}] successful \n'.format( 32 | host['name'], host['ip'], host['port'])) 33 | else: 34 | ssh.close() 35 | logger.error('switch {}[{}:{}] password error \n'.format( 36 | host['name'], host['ip'], host['port'])) 37 | except pexpect.EOF: 38 | ssh.close() 39 | logger.error('switch {}[{}:{}] ssh failed.(EOF) \n'.format( 40 | host['name'], host['ip'], host['port'])) 41 | except pexpect.TIMEOUT: 42 | ssh.close() 43 | logger.error('switch {}[{}:{}] ssh failed.(TIMEOUT) \n'.format( 44 | host['name'], host['ip'], host['port'])) 45 | 46 | 47 | # 华为 交换机配置备份 48 | def HuaweiAutoConfig(host): 49 | ssh = pexpect.spawn('ssh -o StrictHostKeyChecking=no -p %s %s@%s ' % 50 | (host['port'], host['username'], host['ip'])) 51 | 52 | try: 53 | ssh.expect('[Pp]assword:', timeout=30) 54 | ssh.sendline(host['password']) 55 | ssh_info = ssh.expect(['>$', '[Pp]assword:']) 56 | if ssh_info == 0: 57 | ssh.sendline('screen-length 0 temporary') 58 | ssh.expect(['>$', ']$']) 59 | ssh.sendline('dis cur') 60 | ssh.expect(['>$', ']$']) 61 | with open(log_filename + '/' + host['ip'] + '.txt', 'w') as f: 62 | print(ssh.before.decode('utf8', 'ignore').replace('\r', ''), 63 | file=f) 64 | ssh.close() 65 | logger.info('backup switch {}[{}:{}] successful \n'.format( 66 | host['name'], host['ip'], host['port'])) 67 | else: 68 | ssh.close() 69 | logger.error('switch {}[{}:{}] password error \n'.format( 70 | host['name'], host['ip'], host['port'])) 71 | except pexpect.EOF: 72 | ssh.close() 73 | logger.error('switch {}[{}:{}] ssh failed.(EOF) \n'.format( 74 | host['name'], host['ip'], host['port'])) 75 | except pexpect.TIMEOUT: 76 | ssh.close() 77 | logger.error('switch {}[{}:{}] ssh failed.(TIMEOUT) \n'.format( 78 | host['name'], host['ip'], host['port'])) 79 | 80 | 81 | # 锐捷/思科 交换机配置备份 82 | def RuijieAutoConfig(host): 83 | ssh = pexpect.spawn('ssh -o StrictHostKeyChecking=no -p %s %s@%s ' % 84 | (host['port'], host['username'], host['ip'])) 85 | 86 | try: 87 | ssh.expect('[Pp]assword: ', timeout=30) 88 | ssh.sendline(host['password']) 89 | ssh_info = ssh.expect(['#$', '[Pp]assword:']) 90 | if ssh_info == 0: 91 | ssh.sendline('terminal length 0') 92 | ssh.expect('#$') 93 | ssh.sendline('sh run') 94 | ssh.expect('#$') 95 | with open(log_filename + '/' + host['ip'] + '.txt', 'w') as f: 96 | print(ssh.before.decode('utf8', 'ignore').replace('\r', ''), 97 | file=f) 98 | ssh.close() 99 | logger.info('backup switch {}[{}:{}] successful \n'.format( 100 | host['name'], host['ip'], host['port'])) 101 | else: 102 | ssh.close() 103 | logger.error('switch {}[{}:{}] password error \n'.format( 104 | host['name'], host['ip'], host['port'])) 105 | except pexpect.EOF: 106 | ssh.close() 107 | logger.error('switch {}[{}:{}] ssh failed.(EOF) \n'.format( 108 | host['name'], host['ip'], host['port'])) 109 | except pexpect.TIMEOUT: 110 | ssh.close() 111 | logger.error('switch {}[{}:{}] ssh failed.(TIMEOUT) \n'.format( 112 | host['name'], host['ip'], host['port'])) 113 | 114 | 115 | # 检测主机端口是否连通 116 | def host_alive(host): 117 | i = os.system('nc -zv {} {} -w 5'.format(host['ip'], host['port'])) 118 | if i != 0: 119 | logger.error('switch {}[{}:{}] is unreachable \n'.format( 120 | host['name'], host['ip'], host['port'])) 121 | return False 122 | return True 123 | 124 | 125 | def backup(host): 126 | if host['type'] == 'h3c': 127 | H3CAutoConfig(host) 128 | elif host['type'] == 'huawei': 129 | HuaweiAutoConfig(host) 130 | elif host['type'] == 'ruijie': 131 | RuijieAutoConfig(host) 132 | elif host['type'] == 'cisco': 133 | RuijieAutoConfig(host) 134 | else: 135 | logger.error('switch {}[{}:{}] is not support \n'.format( 136 | host['name'], host['ip'], host['port'])) 137 | 138 | 139 | # load config file 140 | config_file = pathlib.Path(r'./hosts.yaml') 141 | yaml = YAML(typ='safe') 142 | config = yaml.load(config_file) 143 | 144 | date_date = datetime.datetime.now().strftime('%Y-%m-%d') 145 | log_filename = config['backup_path'] + os.sep + date_date 146 | os.system('mkdir -p %s' % log_filename) 147 | 148 | # init logger 149 | logger = logging.getLogger() 150 | logger.setLevel('DEBUG') 151 | BASIC_FORMAT = "%(asctime)s:%(levelname)s:%(message)s" 152 | DATE_FORMAT = '%Y-%m-%d %H:%M:%S' 153 | formatter = logging.Formatter(BASIC_FORMAT, DATE_FORMAT) 154 | chlr = logging.StreamHandler() # 输出到控制台的handler 155 | chlr.setFormatter(formatter) 156 | chlr.setLevel('DEBUG') # 也可以不设置,不设置就默认用logger的level 157 | fhlr = logging.FileHandler(filename=log_filename + '/error-' + str(date_date) + 158 | '.log', 159 | mode='a+', 160 | encoding='utf8') # 输出到文件的handler 161 | fhlr.setFormatter(formatter) 162 | logger.addHandler(chlr) 163 | logger.addHandler(fhlr) 164 | 165 | cpus = cpu_count() 166 | 167 | alive_host = [] 168 | 169 | if len(sys.argv) > 1: 170 | for host in config['hosts']: 171 | if host['ip'] in sys.argv[1:] and host_alive(host): 172 | alive_host.append(host) 173 | else: 174 | for host in config['hosts']: 175 | if host_alive(host): 176 | alive_host.append(host) 177 | 178 | with concurrent.futures.ThreadPoolExecutor(max_workers=cpus) as executor: 179 | submits = [executor.submit(backup, host) for host in alive_host] 180 | for future in concurrent.futures.as_completed(submits): 181 | future.result() 182 | 183 | nowtime = datetime.datetime.now() 184 | # 删除过期的配置文件 185 | backup_filelist = list(os.listdir(config['backup_path'])) 186 | for backup_file in backup_filelist: 187 | filetime = datetime.datetime.fromtimestamp( 188 | os.path.getmtime(config['backup_path'] + os.sep + backup_file)) 189 | if (nowtime - filetime).seconds > config['keep_time'] * 24 * 3600: 190 | logger.info('delete backup files before {} days[{}]'.format( 191 | config['keep_time'], filetime)) 192 | os.system('rm -rf ' + config['backup_path'] + os.sep + backup_file) 193 | --------------------------------------------------------------------------------