基于python netmiko通过ssh备份网络设备配置

自己为了便利写出来的基于python netmiko去ssh备份网络设备配置,用过secureCRT的脚本去备份设备配置,但是它没有图形化界面,使用不方便,自己就重新用python开发了一个,同时用pyinstaller打包成可执行程序(这里就不说明怎么打包了,搜一下就出来了,不打包也行,看你的)。感觉netmiko这个包还是很强大,大部分的设备都支持,不支持的也可以找到相似的去实现,比如我这里迈普的设备就是这样。该项目需要对netmiko和pyqt5有一个基本的了解,才能根据本人所写的去实现自定义的需求,代码中也有相应的注释。控制线程个数本来是用线程池,但是pyinstaller打包后有些线程无法执行完成,不知道啥原因,用信号量代替就正常了。

以下是几个源代码和配置文件的简单说明:

  1. main.py,程序的入口
  2. main_ui.py,图形化界面代码,由qtdesigner生成并加以修改
  3. backup_cur.py,具体功能的逻辑代码
  4. 备份设备列表.txt,目前只支持华为、迈普、锐捷、锐捷AC的配置备份(其他类型的设备自己去实现就好,不难,已经有一个框架了),分别对应配置文件中的Huawei、Mypower、Ruijie、RuijieAC,配置文件每一个设备占用一行,每一行的字段包含设备类型、IP地址、设备名称,用空格分开。设备类型、IP地址对了就行,设备名称随便起,但不能是空。务必严格遵循配置文件的语法,否则可能无法备份配置。具体可参考下面的文件示例。
  5. 在还没有熟悉代码前,所有文件名称最好别变,否则无法使用。注意代码中的注释,程序不难。

以下是各个源文件和文件

main.py,程序的入口

# @Time        : 2022/12/17
# @Author      : zhu
# @Description : 程序入口
from sys import argv, exit
from PyQt5.QtWidgets import QApplication, QMainWindow
from main_ui import UiMainWindow
from backup_cur import BackupCur

if __name__ == '__main__':
    app = QApplication(argv)
    window = QMainWindow()
    ui = UiMainWindow(window)
    backup_cur = BackupCur(ui)  # 变量虽然不用但必须声明,否则不生效
    window.show()
    exit(app.exec_())

main_ui.py,图形化界面代码

# @Time        : 2022/12/17
# @Author      : zhu
# @Description : UI界面代码
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QAction


class UiMainWindow(object):
    """由PyQt5 UI code generator 5.15.9生成"""

    def __init__(self, main_window):
        """初始化

        :param main_window: QMainWindow类"""
        main_window.setObjectName('main_window')
        main_window.resize(1200, 800)
        main_window.setStyleSheet('QPushButton{background-color: rgb(0, 102, 179); color: rgb(255,255,255)}')
        self.central_widget = QtWidgets.QWidget(main_window)
        self.central_widget.setObjectName('central_widget')
        self.vertical_layout_3 = QtWidgets.QVBoxLayout(self.central_widget)
        self.vertical_layout_3.setObjectName('vertical_layout_3')
        self.stacked_widget = QtWidgets.QStackedWidget(self.central_widget)
        self.stacked_widget.setObjectName('stacked_widget')

        self.main_page = QtWidgets.QWidget()  # 主页
        self.main_page.setObjectName('main_page')
        self.horizontal_layout_2 = QtWidgets.QHBoxLayout(self.main_page)
        self.horizontal_layout_2.setObjectName('horizontal_layout_2')
        self.horizontal_layout_1 = QtWidgets.QHBoxLayout()
        self.horizontal_layout_1.setObjectName('horizontal_layout_1')
        self.label_1 = QtWidgets.QLabel(self.main_page)
        font = QtGui.QFont()
        font.setFamily('Agency FB')
        font.setPointSize(36)
        self.label_1.setFont(font)
        self.label_1.setAlignment(QtCore.Qt.AlignCenter)
        self.label_1.setWordWrap(True)
        self.label_1.setObjectName('label_1')
        self.horizontal_layout_1.addWidget(self.label_1)
        self.horizontal_layout_2.addLayout(self.horizontal_layout_1)
        self.stacked_widget.addWidget(self.main_page)

        self.backup_cur_page = QtWidgets.QWidget()  # 备份设备配置界面
        self.backup_cur_page.setObjectName('backup_cur_page')
        self.horizontal_layout_4 = QtWidgets.QHBoxLayout(self.backup_cur_page)
        self.horizontal_layout_4.setContentsMargins(0, 0, 0, 0)
        self.horizontal_layout_4.setObjectName('horizontal_layout_4')
        self.vertical_layout_2 = QtWidgets.QVBoxLayout()
        self.vertical_layout_2.setObjectName('vertical_layout_2')
        self.backup_cur_button = QtWidgets.QPushButton(self.backup_cur_page)
        self.backup_cur_button.setObjectName('backup_cur_button')
        self.vertical_layout_2.addWidget(self.backup_cur_button)
        self.horizontal_layout_4.addLayout(self.vertical_layout_2)
        self.backup_plain_text_edit = QtWidgets.QPlainTextEdit(self.backup_cur_page)
        self.backup_plain_text_edit.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
        self.backup_plain_text_edit.setReadOnly(True)
        self.backup_plain_text_edit.setObjectName('backup_plain_text_edit')
        self.horizontal_layout_4.addWidget(self.backup_plain_text_edit)
        self.horizontal_layout_4.setStretch(0, 1)
        self.horizontal_layout_4.setStretch(1, 5)
        self.stacked_widget.addWidget(self.backup_cur_page)

        self.vertical_layout_3.addWidget(self.stacked_widget)  # 菜单栏
        main_window.setCentralWidget(self.central_widget)
        self.menu_bar = QtWidgets.QMenuBar(main_window)
        self.menu_bar.setGeometry(QtCore.QRect(0, 0, 1200, 26))
        self.menu_bar.setStyleSheet(
            'QMenuBar::item::selected{background-color: rgb(0, 102, 179); color: rgb(255,255,255)}')
        self.menu_bar.setObjectName('menu_bar')
        self.main_menu = QAction()  # 跳过传统的方式创建菜单栏的菜单项,利用这种可以绑定点击出发时的要做的动作
        self.main_menu.setObjectName('main_menu')
        self.main_menu.triggered.connect(self.set_current_main)  # 按下首页菜单时设置栈布局当前界面为首页
        self.backup_cur_menu = QAction()
        self.backup_cur_menu.setObjectName('backup_cur_menu')
        self.backup_cur_menu.triggered.connect(self.set_current_backup_cur)  # 按下首页菜单时设置栈布局当前界面为备份设备配置
        main_window.setMenuBar(self.menu_bar)
        self.menu_bar.addAction(self.main_menu)
        self.menu_bar.addAction(self.backup_cur_menu)

        self.retranslate_ui(main_window)
        self.stacked_widget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(main_window)

    def retranslate_ui(self, main_window) -> None:
        """自定义字段设置

        :param main_window: QMainWindow类"""
        _translate = QtCore.QCoreApplication.translate
        main_window.setWindowTitle(_translate('main_window', '备份设备配置'))
        self.label_1.setText(_translate('main_window', '未经允许不得擅自使用,否则后果自负'))
        self.backup_cur_button.setText(_translate('main_window', '开始备份'))
        self.main_menu.setText(_translate('main_window', '首页'))
        self.backup_cur_menu.setText(_translate('main_window', '备份设备配置'))

    def set_current_main(self):
        """设置栈布局当前界面为首页"""
        self.stacked_widget.setCurrentIndex(0)

    def set_current_backup_cur(self):
        """设置栈布局当前界面为备份设备配置"""
        self.stacked_widget.setCurrentIndex(1)

backup_cur.py,具体功能的逻辑代码

# @Time        : 2022/12/17
# @Author      : zhu
# @Description : 备份具体实现
# @update      : 使用pyqt制作图形化界面
import os
import datetime
import threading
import traceback
from socket import inet_pton, AF_INET, AF_INET6
from threading import Thread
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QMessageBox
from netmiko import ConnectHandler
from main_ui import UiMainWindow


def is_ip_address(ip: str) -> bool:
    """判读给定的字符串是否是IPv4或IPv6地址

    :param ip: IP地址字符串"""
    try:
        inet_pton(AF_INET, ip)
        return True
    except:
        try:
            inet_pton(AF_INET6, ip)
            return True
        except:
            return False


class BackupCur(QObject):
    """目前支持华为、迈普、锐捷、锐捷AC,对应配置文件中的Huawei、Mypower、Ruijie、RuijieAC,
    设备类型必须和以上说明的配置文件类型严格对上,否则无法备份。控制线程个数本来是用线程池,
    但是pyinstaller打包后有些线程无法执行完成,就使用信号量代替了"""
    BACKUP_DEVICE_FILE = './备份设备列表.txt'
    USERNAME = 'your_username'
    PASSWORD = 'your_password'
    SSH_PORT = 22
    SEM = threading.Semaphore(10)  # 信号量为10,这里限制线程的最大数量为10个
    MY_SIGNAL = pyqtSignal(list)  # 该信号用于向主线程传递信息更新UI界面,信息类型标识、信息体

    def __init__(self, ui_main_window: UiMainWindow):
        """初始化

        :param ui_main_window: UiMainWindow类"""
        super().__init__(None)
        self.failed_count = 0  # 统计备份失败的设备个数
        self.ui = ui_main_window
        self.thread = None  # 备份配置的子线程
        self.finished_device_count = 0  # 统计备份完成的设备数量
        self.backup_path = ''
        self.device_count = 0  # 备份设备总数
        self.ui = ui_main_window
        self.ui.backup_cur_button.clicked.connect(self.confirm_backup)
        self.MY_SIGNAL.connect(self.my_slot)

    def confirm_backup(self) -> None:
        """确认是否进行备份"""
        message_box = QMessageBox(QMessageBox.Question, '提示', '确认备份设备吗')  # 自定义提示对话框
        yes = message_box.addButton(message_box.tr('确定'), QMessageBox.YesRole)
        message_box.addButton(message_box.tr('取消'), QMessageBox.NoRole)
        message_box.exec_()
        if message_box.clickedButton() == yes:  # 选择确定
            self.finished_device_count = 0
            self.failed_count = 0
            Thread(target=self.start_backup).start()  # 生成子线程避免主线程卡顿

    def start_backup(self) -> None:
        """开始进行备份"""
        self.MY_SIGNAL.emit([0])
        self.backup_path = './操作日志/' + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '/'
        if not os.path.isdir(self.backup_path):
            os.makedirs(self.backup_path)

        try:
            with open(self.BACKUP_DEVICE_FILE) as file:
                device_list = file.readlines()  # 按行读取文件
                self.device_count = len(device_list)
                for device in device_list:
                    self.SEM.acquire()  # 获取信号量,保证最多只有10个线程同时运行
                    self.thread = threading.Thread(target=self.worker, args=(device,))
                    self.thread.start()
        except OSError:  # 文件打开失败
            self.MY_SIGNAL.emit([1])

    def worker(self, worker_device: str) -> None:
        """备份设备的线程

        :param worker_device: 备份的设备信息列表,类型、IP、设备名"""
        device_fields = worker_device.split()
        for i in range(len(device_fields)):
            device_fields[i] = device_fields[i].replace('\n', '').replace('\r', '')
        try:
            if is_ip_address(device_fields[1]):
                device_type = device_fields[0]
                if device_type == 'Huawei':
                    ssh_type = 'huawei'
                    cmd = 'dis cur'
                else:
                    ssh_type = 'ruijie_os'  # Ruijie MaipuRT RuijieAC
                    cmd = 'show run'

                connect_filed = {  # ssh各个字段
                    'device_type': ssh_type,
                    'host': device_fields[1],
                    'username': self.USERNAME,
                    'password': self.PASSWORD,
                    'port': self.SSH_PORT,
                    'session_log': self.backup_path + device_fields[2] + '(' + device_fields[1] + ').log'
                }

                device_name = device_fields[2]
                try:
                    net_connect = ConnectHandler(**connect_filed)  # ssh连接
                    if device_type == 'Huawei' or device_type == 'Ruijie':
                        net_connect.send_command(command_string=cmd,
                                                 read_timeout=20.0,
                                                 strip_prompt=False,  # 输出结果包含设备提示
                                                 strip_command=False)  # 输出结果包含输入的命令
                    elif device_type == 'Mypower':
                        net_connect.send_command(command_string='more off',
                                                 expect_string=device_name + '#',
                                                 strip_prompt=False,
                                                 strip_command=False)
                        net_connect.send_command(command_string=cmd,
                                                 expect_string=device_name + '#')
                    else:  # 锐捷AC
                        net_connect.send_command(command_string=cmd,
                                                 expect_string=device_name + '#')
                        net_connect.send_command(command_string='show ap-config running',
                                                 expect_string=device_name + '#')
                    net_connect.disconnect()
                except:
                    traceback.print_exc()
                    self.MY_SIGNAL.emit([3, device_name, device_fields[1]])
        except:  # 一般来说只会是由于配置文件的设备字段出错导致,数组越界
            self.MY_SIGNAL.emit([2, worker_device])
        finally:
            self.SEM.release()  # 释放信号量
            self.MY_SIGNAL.emit([4])

    def my_slot(self, args: list) -> None:
        """槽函数,接收子线程信号并更改主界面UI

        :param args: 标识位、信息、设备名称、设备ip地址、未知设备"""
        if args[0] == 0:
            self.ui.backup_plain_text_edit.appendPlainText('正在备份设备配置,请稍后...')
            self.ui.backup_cur_button.setEnabled(False)  # 设置按钮不可用以及颜色为灰色
            self.ui.backup_cur_button.setStyleSheet(
                'QPushButton{background-color: rgb(128, 128, 128); color: rgb(255,255,255)}')
            self.ui.backup_cur_button.repaint()  # 重新绘制按钮,否则样式可能不生效
        elif args[0] == 1:
            self.ui.backup_plain_text_edit.appendPlainText(
                '打开文件' + os.path.abspath(self.BACKUP_DEVICE_FILE) + '出错,请检查配置文件是否存在!')
            self.ui.backup_cur_button.setEnabled(True)  # 备份完成恢复按钮为原样
            self.ui.backup_cur_button.setStyleSheet(
                'QPushButton{background-color: rgb(1, 102, 179); color: rgb(255,255,255)}')
            self.ui.backup_cur_button.repaint()  # 重新绘制按钮,否则样式可能不生效
        elif args[0] == 2:
            self.failed_count += 1
            self.ui.backup_plain_text_edit.appendPlainText('未知设备(' + args[1] + ')备份配置失败')
        elif args[0] == 3:
            self.failed_count += 1
            self.ui.backup_plain_text_edit.appendPlainText(args[1] + '(IP地址:' + args[2] + ')' + '备份配置失败')
        elif args[0] == 4:
            self.finished_device_count += 1
            if self.finished_device_count == self.device_count:
                self.ui.backup_plain_text_edit.appendPlainText(
                    '备份设备完成!其中成功备份的设备为' + str(self.device_count - self.failed_count) +
                    '台,失败的为' + str(self.failed_count) + '台')
                self.ui.backup_plain_text_edit.appendPlainText(
                    '可前往[' + os.path.abspath(self.backup_path) + ']查看备份结果')
                self.ui.backup_cur_button.setEnabled(True)  # 备份完成恢复按钮为原样
                self.ui.backup_cur_button.setStyleSheet(
                    'QPushButton{background-color: rgb(1, 102, 179); color: rgb(255,255,255)}')
                self.ui.backup_cur_button.repaint()  # 重新绘制按钮,否则样式可能不生效
        self.ui.backup_plain_text_edit.repaint()  # 重新绘制,否则样式可能不生效

备份设备列表.txt

RuijieAC 192.168.0.1 RUIJIE_AC_1
Mypower 192.168.0.2 MAIPU_RT_1
Ruijie 192.168.0.3 RUIJIE_SW_1
Huawei 192.168.0.4 HUAWEI_RT_1
  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值