Приветствую, друг!

Обещал себе, что нужно тратить больше времени на автоматизацию в 2024-м, пора начать выполнять.

Проблема изучения любого языка программирования - практика. Опытные дядьки скажут - отличная практика - писать код на работе. Выбери себе задачу и попробуй решить. Оно в целом да, но когда садишься думать “а что же такого написать” оказывается это не так и просто.

На досуге в ЛС пришел коллега и спросил “смогешь написать скрипт, который будет собирать с коробок аптайм, сортировать их и выводить в каком-то виде?”

Задача вроде понятна, надо пробежаться по коробкам (например используя nornir+scrapli) собрать вывод в структуру данных (например список/словарь etc), нормализовать аптайм, чтобы его можно было сравнивать, прикрутить сравнение и решить куда вывести полученное.

Ниже постарался описать, что у меня получилось :) Критика и пожелания приветствуется, начнем пока без ООП.

Код писал под SNR-ы, которые так удачно подвернулись в количестве трех штук. По причине различия вывода uptime и его расположение в разных вендорах, код будет отличаться, поэтому, думается мне, логичнее потом все это будет оформить в отдельный класс и наплодить внутри подклассы/методы на каждого вендора, но пока, как я уже сказал выше, без ООП и на одном вендоре.

Если пишите новый инструмент автоматизации, пишите сначала для одной коробки и одного вендора, такой сетап легче тестировать и фокус с задачи не сбивается на всякие межвендорные различия.

Что будем использовать? Nornir, модуль nornir-scrapli и python с его встроенными модулями.

Nornir - это фреймворк для написания Python-скриптов для автоматизации сетевых задач. Он предоставляет удобные средства для управления устройствами в сети, включая выполнение команд, фильтрацию устройств и многое другое.

Если не знакомы с Nornir, я писал о нем в отдельной заметке.

Scrapli - это библиотека Python для взаимодействия с устройствами через протоколы, такие как SSH. Похожа на netmiko, только лучше. Подробно рассказывать пока не буду т.к логика работы скрыта за модулем норнира - nornir-scrapli.

Должен ли сетевик кодить аки разработчик? Скорее нет, чем да, но как минимум, будем придерживаться следующих принципов:

  • продумываем заранее, что хочется видеть после выполнения с кода;
  • соблюдаем PEP8;
  • стараемся держаться подальше от спагетти-кода;
  • стремимся разделять код на модули, для удобства восприятия;
  • вносим базовые проверки, чтобы перехватить трейсбэки и пропускать проблемные коробки, вместо “падения” скрипта.

Напишет ли условный ChatGPT этот скрипт? Да, на 90% это будет рабочий код, важно лишь задать верный контекст и снабдить нейронку исходными данными. Останется только адаптировать под задачу и готово.

Есть ли тогда смысл разбирать все это? Мое мнение, конечно стоит. Нейронки без работы не останутся, а ты, друг мой, как инженер, нуждаешься в скиллах, которые дадут тебе хлеб, немного масла и конкурентные преимущества.

Всяческие рекомендации опробовать тот же ChatGPT и использовать его как помощника в изучении. Годится для генерация примеров, объяснения концепций кода, генерация кусков кода, код-ревью и прочие мелочи. Точно будет стойкое желание “а я ща кааак скопирую и пойду курить бамбук”, но это неправильная стратегия. Вот как стоит сделать:

  • перепиши руками
  • вникни в происходящее
  • выпиши неизвестные конструкции и разбери в том же ipython или pdbr по частям
  • сходи обсудить в группу если требуется мнение со стороны :)

Весь код можно будет найти на гитхабе. Комментарии и PR приветствуются.

Разбираем код

Пройдемся по коду и разберем основные моменты и нюансы.

import re

from nornir import InitNornir
from nornir.core.filter import F
from nornir_scrapli.tasks import (
    send_command,
)
from nornir.core.exceptions import NornirExecutionError

Здесь мы импортируем необходимые библиотеки и модули. re используется для работы с регулярными выражениями, а Nornir и Scrapli - для автоматизации задач сетевого взаимодействия, что есть что:

  • InitNornir - иницилизирует сам Nornir;
  • F - функция продвинутой фильтрации
  • send_command - отправка show команд через Scrapli
  • NornirExecutionError - Nornir перехватит исключение, если какая-либо из задач завершится сбоем.

Код модульный, каждая функция выполняет определенное действие. В дальнейшем с таким кодом будет удобнее работать. Импорты сделали, теперь пробежимся по каждой функции отдельно.

def get_uptime(task):
    '''
    Получает информацию о времени работы устройства.

    :param task: Объект задачи Nornir
    :return: Общее время работы устройства в минутах или None в случае ошибки

    '''
    regex = r'\w+ +(?P<weeks>\d+).+?(?P<days>\d+).+?(?P<hours>\d+).+?(?P<minutes>\d+)' 
    try:
        get_version = task.run(
            task=send_command,
            command="sh ver",
        )

        current_uptime = get_version.result.split('\n')[-1]
        match_uptime = re.search(regex, current_uptime)
        if match_uptime:
            weeks, days, hours, minutes = map(int, match_uptime.groups())
            total_minutes = weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + minutes
            return total_minutes
    except NornirExecutionError as error:
        print(f"Error accessing device {task.host.name}: {str(error)}")
    return None

Функция get_uptime содержит основную логику по сбору uptime с устройств.

Тут мы пишем регулярное выражение и “присваиваем” его переменнойregex. Регуляркой будем искать в строке формата Uptime is 3 weeks, 6 days, 2 hours, 36 minutes, которую вытянем из вывода sh version. Еще с курса Наташи я начал писать регулярки в regex.101, собсна там и продолжаю. Не забываем тыкнуть Python в Flavor.

Для выдергивания вывода sh version используем модуль nornir-scrapli, через отправку команды show version на устройство.

Результат выполнения таски будет хранится в переменной get_version в виде строки, оттуда и вырезаем нужный нам кусок с uptime.

Вывод команды с оборудования, чаще всего, получаем в виде единой строки с пробельными символами, например \t\n\r\f\v:

(Pdbr) result['R1'][1].result
'  SNR-S2982G-24T-POE-E Device, Compiled on May 07 19:33:53 2023\n  sysLocation Building 57/2,Predelnaya st, Ekaterinburg,
Russia\n  CPU Mac f8:f0:82:d3:b8:5e\n  Vlan MAC f8:f0:82:d3:b8:5d\n  SoftWare Version 7.0.3.5(R0241.0594)\n  BootRom Version
7.2.55\n  HardWare Version 1.0.1\n  CPLD Version N/A\n  Serial No.:SW083310MA15000036\n  Copyright (C) 2023 NAG LLC\n  All
rights reserved\n  Last reboot is cold reset.\n  Uptime is 4 weeks, 0 days, 0 hours, 19 minutes'

Нам нужно выгрызть только uptime и затем пробежаться регуляркой, либо можно написать регулярку на весь вывод, тогда не придется приседать с split/strip и пр. методами по работе со строками, выбор за вами. То, что нам интересно:

Uptime is 4 weeks, 0 days, 0 hours, 19 minutes

Т.к это строка, в ход идут популярные методы работы со строками, тут нам подходит split. С его помощью можно разбить строку на несколько элементов, которые соберутся в список. Наш uptime окажется последним элементом в списке, который мы можем гарантированно забрать обращением к списку через индекс [-1]. Получим следующее:

(Pdbr) result['R1'][1].result.split('\n')
[
    'SNR-S2982G-24T-POE-E Device, Compiled on May 07 19:33:53 2023',
    '  sysLocation Building 57/2,Predelnaya st, Ekaterinburg, Russia',
    '  CPU Mac f8:f0:82:d3:b8:5e',
    '  Vlan MAC f8:f0:82:d3:b8:5d',
    '  SoftWare Version 7.0.3.5(R0241.0594)',
    '  BootRom Version 7.2.55',
    '  HardWare Version 1.0.1',
    '  CPLD Version N/A',
    '  Serial No.:SW083310MA15000036',
    '  Copyright (C) 2023 NAG LLC',
    '  All rights reserved',
    '  Last reboot is cold reset.',
    '  Uptime is 4 weeks, 0 days, 0 hours, 19 minutes'
]

Добавим индекс [-1] и получим необходимую строку:

(Pdbr) result['R1'][1].result.strip().split('\n')[-1]
'  Uptime is 4 weeks, 0 days, 0 hours, 19 minutes'

Проверка с if нужна, чтобы исключить ошибки, если совпадения в re, по какой-то причине, не было. А вот если данные успешно найдены, преобразует их в минуты и возвращает общее время работы в минутах.

Тут может быть интересна вот эта строка:

weeks, days, hours, minutes = map(int, match_uptime.groups())

С помощью функции map мы не только распакуем все 4 элемента, которые собрались в регулярке, но и превратим их из строки в целые числа и все это в одной строке. Нам нужны именно числа т.к дальше будут математические операции, что логично.

def sort_devices(device_info):
    return device_info[1]

sort_devices - это функция сортировки устройств по времени работы. Она возвращает время работы для каждого устройства, что позволяет отсортировать устройства по этому критерию. Зачем? Удобно запихнуть в параметр key в функции sorted() который аля фильтр. Уверен, можно запилить лямбда-функцией, но я пока не дорос.

def format_uptime(device_info):
    '''
    Форматирует информацию о времени работы устройства для вывода.

    :param device_info: Кортеж (хост, время работы в минутах)
    :return: Строка с информацией о времени работы

    '''
    host, uptime_minutes = device_info
    
    if not isinstance(uptime_minutes, int):
        print(f"Error: Unexpected data type for uptime_minutes on {host}. Skipping.")
        return None

    weeks, days = divmod(uptime_minutes, 7 * 24 * 60)
    days, hours = divmod(days, 24 * 60)
    hours, minutes = divmod(hours, 60)
    return f"Device {host} uptime is {weeks} weeks, {days} days, {hours} hours, {minutes} minutes."

format_uptime - это функция форматирования uptime устройства. Она использует divmod для разбиения времени в минутах на недели, дни, часы и минуты, а затем возвращает отформатированную строку.

Сначала желательно выполнить проверку на соответствие, что uptime_minutes - целое число, иначе получим ошибку и скрипт прекратит выполнение.

Что такое divmod? divmod - это встроенная функция в Python, которая принимает два аргумента и возвращает пару чисел, представляющих результат целочисленного деления и остаток от деления этих двух чисел. Синтаксис функции divmod выглядит следующим образом:

divmod(a, b)

где a и b - это два числа.

Функция divmod выполняет деление a на b и возвращает кортеж из двух значений: результат целочисленного деления (a // b) И остаток от деления (a % b).

Рассмотрим ее работу на примере недель, дней, часов и минут из uptime:

weeks, days = divmod(uptime_minutes, 7 * 24 * 60) 
days, hours = divmod(days, 24 * 60) 
hours, minutes = divmod(hours, 60)

В данном коде uptime_minutes представляет собой общее время работы устройства в минутах.

  1. weeks, days = divmod(uptime_minutes, 7 * 24 * 60): Эта строка преобразует общее количество минут в количество недель и дней. 7 * 24 * 60 представляет собой количество минут в неделе. divmod(uptime_minutes, 7 * 24 * 60) возвращает два значения: количество недель и остаток в минутах после вычета недель.
  2. days, hours = divmod(days, 24 * 60): Затем оставшиеся дни из предыдущего шага делятся на количество минут в сутках (24 * 60), чтобы вычислить количество дней и количество минут, оставшихся после вычета целых суток.
  3. hours, minutes = divmod(hours, 60): Последний шаг делит оставшееся количество минут на 60, чтобы получить количество часов и количество минут после вычета целых часов.

Так мы собираем uptime в нужный вид и выводим его через print с использованием f-строки.

def collect_devices_info(task):
    '''
    Собирает информацию о времени работы устройств из результатов выполнения задач.

    :param task: Результат выполнения задач Nornir
    :return: Список кортежей (хост, время работы в минутах)
    '''
    devices_info = []
    for host, task_result in task.items():
        uptime_minutes = task_result.result
        if uptime_minutes is not None:
            devices_info.append((host, uptime_minutes))
    return devices_info

Функция collect_devices_info собирает список кортежей из хоста и соответствующего ему uptime.

Почему список кортежей? В данном случае, данные, которые мы собираем (хост и время работы в минутах), образуют простую пару, и нет необходимости в сложной структуре типа словаря с ключами и значениями.

Список кортежей удобно передать функции sorted, которая сама прогонит каждый элемент списка и отсортирует по условию, указанному в аргументе key.

def main():
    '''
    Основная функция скрипта.

    Инициализирует Nornir, выполняет задачу получения времени работы, собирает информацию
    и выводит отсортированный результат. Сортировка убывающая.
    '''
    nr = InitNornir(config_file="./config.yaml")
    snr = nr.filter(F(groups__contains="snr"))
    result = snr.run(get_uptime)

    devices_info = collect_devices_info(result)
    sorted_devices = sorted(devices_info, key=sort_devices, reverse=True)
    for device_info in sorted_devices:
        print(format_uptime(device_info))

Функция main - блок с кодом, где вызываются все остальные, ранее объявленные, функции.

Здесь инициализируем объект Nornir, фильтруем устройства по группе snr, передаем Nornir-у функцию get_uptime в качестве задачи, которая будет выполнена на устройствах только из группы snr. После того, как таска отработает, результат будет хранится в переменной result. Прогоним его через функцию collect_devices_info и получим список кортежей в формате [("R1", 310), (...)]. И уже после переходим к передаче полученного списка в функцию sorted. Останется оформить все в какой-нибудь вывод, для этого прогоним отсортированный список через цикл, внутри которого используем функцию format_uptime, которая сформирует итоговый результат вида:

Device R1 uptime is 3 weeks, 6 days, 15 hours, 29 minutes.
Device R2 uptime is 3 weeks, 1 days, 3 hours, 26 minutes.
Device R3 uptime is 3 weeks, 1 days, 3 hours, 26 minutes.

Ну и под конец конструкция if __name__ == "__main__", которая должна быть в каждом скрипте, если планируете дальше импортировать функции в этом коде как модули в другом коде. В таком случае все, что находится в функции main отработает только тогда, когда код запускается напрямую.

Криво косо, но работает! И это важный момент на начальном этапе. Пробовать, пробовать и еще раз пробовать. Говнокодить придется, это часть процесса обучения. Все через это проходят.


Хочешь обсудить тему?

С вопросами, комментариями и/или замечаниями, приходи в чат или подписывайся на Telegram-канал.