Централизованный сбор конфигураций с MikroTik'ов средствами Python

    Зачем



    С ростом количества сетевых узлов и сложности их конфигурации наверняка у многих возникает вопрос — а если железка умрет, смогу ли я быстро восстановить работу на другой? Если смогу сделать это вручную, то как долго я буду потом отлавливать мелочи, которые забыл?

    Процесс гугления привел к пониманию того, что конкретно для этого вендора готовых решений подобного рода нет. То, что было найдено в комьюнити мне не понравилось — предлагали размещать скрипты резервного копирования на самой железке и запускать их по расписанию. По сути задачу это решало, но если у меня несколько десятков единиц оборудования, нащелкивать копипасту по каждому экземпляру — это не по Фен-Шую.

    Благо есть небольшой навык написания полезняшек на Python и фантазия.


    Фантазия



    В уме прикидываю что понадобится — TFTP сервер, куда будем складывать наше хозяйство, *nix машинка (с ней проще, чем под win всё это делать), Python на борту с необходимым набором библиотек. Как выяснилось впоследствии, TFTP сервер можно выкинуть, понадобится FTP.

    Попытка раз


    Пробую решить задачу в лоб — использую telnetlib для связи — не работает. Странно, ведь с коммутаторами работало. Поразмышляв, понял, что telnet у MikroTik разукрашен всеми цветами радуги, значит сыпет спецсимволами, а фильтровать их в мои планы не входит.

    Попытка два


    Присматриваюсь к библиотеке paramiko и её составляющей SSHClient — теперь всё получается — соединение проходит, могу выполнять команды и получать результат.

    Теперь разберемся как снимаются конфигурации с этих действительно необычных железок. Привычный сценарий на сетевом оборудовании — выполнение одной команды, которая умеет отправлять свой конфиг на TFTP сервер. В случае с MikroTik это вариант не работает — оказывается первым делом бэкап конфига нужно создать:

    /system backup save [name=]

    После этого файл уже можно куда-то загрузить, но как оказалось сделать это можно только по протоколу FTP, а по HTTP и TFTP он умеет только сливать файлы. Это не беда, быстро поднимаем FTP сервер с минимальной конфигурацией, нагуглить которую уважаемым читателям не составит труда.

    /tool fetch address= mode=ftp dst-path= src-path= user= password= upload=yes

    И в конце концов нужно убрать за собой мусор, чтобы не приплыть к исчерпанию свободного места на внутреннем носителе:

    /file remove

    Результат


    За пару часов фантазия родила сей скрипт:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    # for SSH
    from paramiko import SSHClient
    from paramiko import AutoAddPolicy
    # for versioning
    import datetime
    # for file operations
    import os
    # for sleep
    import time
    
    # versioning
    Version = datetime.date.today()
    #print "\n" + str(Version)
    
    # hosts array IP1, IP2, IP3
    hosts = ( "1.2.3.4", "5.6.7.8" , "9.10.11.12")
    # username
    users = ( "user1", "user2", "user3")
    iterUser = iter(users)
    # userpassword
    passwords = ( "pass1", "pass2", "pass3" )
    iterPassword = iter(passwords)
    # FTPD IP
    FtpdIP = "13.14.15.16"
    # ftp user account
    ftpUser = "ftpuser"
    ftpPass = "ftppass"
    # keep backups for 4 weeks
    backtime = datetime.timedelta(weeks=-4)
    sshCli = SSHClient()
    sshCli.set_missing_host_key_policy(AutoAddPolicy())
    
    print "header done"
    
    # loop host adresses
    for host in hosts:
        print "\n" + str(host)
        # iterate through user-password pairs
        user = iterUser.next()
        Password = iterPassword.next()
        # define operations
        CreateLocalBckp = "system backup save name=" + str(host) + "_" + str(Version) + ".backup"
        UploadToFtp = "tool fetch address=" + str(FtpdIP) + " mode=ftp dst-path=" + str(host) + "_" + str(Version) + ".backup src-path=" + str(host) + "_" + str(Version)+ ".backup" + " user=" + str(ftpUser) + " password=" + str(ftpPass) + " upload=yes"
        RemoveLocalBckp = 'file remove "' + str(host) + "_" + str(Version) + ".backup" + '"'
        # try for not to fail the whole script on one error
        try:
            print "connecting.." + str(host) + "@"  + str(user) + ":" + str(Password)
            sshCli.connect(str(host), port=2022, username=str(user), password=str(Password))
            print "connected.."
            # creating local backup
            print "creating local backup.. /" + CreateLocalBckp
            sshCli.exec_command(CreateLocalBckp)
            # sleep after each command because mikrotik can not do it so fast as script executes
            time.sleep(2)
            print "local backup created.."
            # uloading local backup to ftp
            print "uploading local backup to ftp.. /" + UploadToFtp
            sshCli.exec_command(UploadToFtp)
            time.sleep(2)
            print "backup uploaded to remote location.."
            # removing local backup
            time.sleep(2)
            print "removing local backup.. /" + RemoveLocalBckp
            sshCli.exec_command(RemoveLocalBckp)
            time.sleep(2)
            print "local backup removed.."
            sshCli.close()
            # try delete old file (if exists)
            try:
                os.remove("/tftp/" + str(host) + "_" + str(Version + backtime)  +".cfg")
            except:
                print "Error while trying to delete old backup " + "/tftp/" + str(host) + "_" + str(Version + backtime)  +".cfg"
        except:
            print "Error connecting to host", host
    


    Скрипт будет хранить на нашем FTP сервере набор резервных копий конфигураций за месяц с именами вида IP_YYYY-MM-DD.backup

    PS: есть подобное создание для случая коммутаторов 3Com и загрузки их конфигураций через telnet на TFTP — если читателям интересно, обязательно опубликую.
    Поделиться публикацией

    Комментарии 17

      0
      Хорошая статья, респект. Мы используем у себя rancid + cvsweb.
        +4
        Несколько замечаний по стилю, если позволите:

        1. Будьте готовы к тому что в более свежие версии интерпретатора будут ругаться на конструкции print «bla-bla». print — это функция.
        2. «system backup save name=» + str(host) + "_" + str(Version) + ".backup" = «system backup save name={}_{}.backup».format(host, Version).
        3. Убойная конструкция
        # hosts array IP1, IP2, IP3
        hosts = ( «1.2.3.4», «5.6.7.8», «9.10.11.12»)
        # username
        users = ( «user1», «user2», «user3»)
        iterUser = iter(users)
        # userpassword
        passwords = ( «pass1», «pass2», «pass3» )
        iterPassword = iter(passwords)
        for host in hosts:
        print "\n" + str(host)
        # iterate through user-password pairs
        user = iterUser.next()
        Password = iterPassword.next()

        легко и просто заменяется на
        creds = ((«1.2.3.4», «user1», «pass1»), («1.2.3.3», «user2», «pass2»), («1.2.3.5», «user3», «pass3») )
        for (host, user, pass) in creds:



        имхо, Python — очень красивый и лаконичный язык, если уметь его готовить. :)
          +1
          тогда уж:

          hosts = { '1.2.3.4': { 'user': 'user1', 'password': 'password1'},… }
            +1
            Ну это уж на вкус и цвет, кому как больше нравится.
              +1
              Имхо, довольно многословно получится в итоге. Особенно, когда в теле цикла начнете обращаться к значениям.
                +1
                В итоге у автора получится fabric :)
                  +1
                  Вообще говоря, для такой задачи Python — это оверкилл. Все тоже самое довольно легко делается на шелл-скриптах. Но в качестве упражнения и демонстрации работы с paramiko — вполне неплохо.
            0
            Насчет более свежих версий интерпретатора имеется в виду версия 3.х? Просто я писал в 2.7 — вроде как последняя на сегодняшний день версия, устанавливаемая по дефолту.
            Второй пункт пожалуй дело вкуса, мне удобнее наблюдать параметры по ходу следования строки, хотя, согласен, возможно этот стиль написания кода и неправославен.
            Абсолютно согласен насчет итераторов, приведу в божеский вид на один трехмерный массив обязательно.
            Самому не понравилась конструкция в собственном исполнении, но поэтому в топике указал — "… есть небольшой навык..."
              +1
              Строго говоря, print() или print — это дело вкуса, ибо на этот счет нет никаких строгих рекомендаций. Лично я предпочитаю писать код по возможности совместимый с веткой 3.x, чтобы лишний раз не пользоваться 2to3.py.

              По поводу конкатенации строк. Согласен, дело вкуса, но FYI .format объективно работает в 1,5 раза быстрее чем просто + и явно эффективнее по памяти. В данном конкретном случае это, конечно, не имеет никакого значения.

              Не сочтите за предновогодний наезд. Статья вполне себе полезная, была бы карма — плюсанул бы :)
                0
                Большое спасибо, я адекватно воспринял все замечания. Более того, очень хотелось прочитать как бы тотже функционал реализовал кто-то более опытный.
                С наступающим!
                  0
                  «Лично я предпочитаю писать код по возможности совместимый с веткой 3.x»
                  Разве 2.* форк уже перестаёт быть де-факто стандартом?
                  (без иронии, в практических целях интересно — да, я слышал, что скажем, следующая версия Django будет утилизировать 3.*, но реально, как мне кажется, большинство софта всё ещё использует 2.*, даже скажем не во всех пуританских дистрибутивах Linux ещё стоит 2.7.*.)

                  з.ы. print как функция вместо кейворда — лично я считаю худшим нововведением 3 версии Питона. :( Я люблю этот язык не в последнюю очередь за лаконичность его средств, а лишняя пара скобочек, которую придётся теперь дописывать к зачастую часто используемому оператору — противоречит этой концепции.
                    0
                    Ситуация с переходом между ветками 2.x и 3.х — это замкнутый круг. Python 3.x значительно лучше и продуманнее, но из-за того что он менее развит в отношении кодовой базы (т.е. библиотек и программ) народ предпочитает писать софт под 2.х, что в свою очередь тормозит развитие 3.х. Короче говоря, какую ветку использовать пусть каждый решает сам за себя. Лично я стараюсь делать свой код как можно более совместимым, чтобы избежать головной боли когда наконец настанет светлое будущее и поддержка 2.x будет официально дропнута везде.

                    Разного рода пуританские Linux-дистрибутивы — это вообще постоянный источник проблем когда возникает необходимость установить софт чем более свежий чем окаменелые фекалии мамонта. Благо Python позволяет себя устанавливать в отдельное окружение без глубокой интеграции в систему и собирать пакеты руками через pip или easy_install — был бы gcc и необходимые библиотеки. Конечно, это не так удобно как простой вызов yum (и иже с ним) но это вполне приемлимый способ, на мой взгляд.

                    Использование print как функции с другой стороны дает определенные преимущества (правда, как мне кажется несколько сомнительные), как, например, возможность передачи ее куда-либо в качестве callable-параметра т.п.
              0
              NOC
                +1
                посмотрите в сторону fabric, оно как-раз для подобных случаев заточено
                  0
                  Всего два вопроса (как я понимаю, Вы администратор этого зоопарка):
                  — как часто без Вашего ведома происходит изменение конфигурации оборудования?.. пример: имею небольшой парк роутербордов с микротиком на борту. После каждого изменения в конфигурации делаю копию. Это привычка\правило\аксиома. Измениться конфигурация может автоматически только в направлении блокировки\разблокировки какого-либо правила. Все!
                  — Вы написали, что:

                  «По сути задачу это решало, но если у меня несколько десятков единиц оборудования, нащелкивать копипасту по каждому экземпляру — это не по Фен-Шую.»

                  что мешает написать скрипт, который «скопипастит» это за Вас?
                  В целом способ имеет право на жизнь, нажал плюс — но обломал кармометр. :)

                  P.S. сам был неприятно удивлен, когда узнал про аплоад только по ftp.
                    0
                    Да, дело как-раз таки в том, что я не единственный администратор этого как вы выразились «зоопарка», поэтому о некоторых изменениях я узнаю по факту изменения/поломки. Кроме того — у меня их на самом деле много — несколько десятков.
                    Копипастить скрипт на каждую железку другим скриптом… дело вкуса, но мне кажется маршрутизатор должен заниматься маршрутизацией, а не слежением за своей резервной копией. Еще стараюсь придерживаться правила do it with less administrative effort.
                    На самом деле было бы интересно взглянуть на вашу версию решения этой задачи — вполне можно совмещать оба подхода.
                    0
                    Делал тоже самое на перле
                    sysadminblog.ru/blog/linux/139.html
                    Только добавил еще, чтобы св конце скрипт посылал отчет, кого сбекапил, кого не смог, на всякий случай

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое