Приветствую, друг! Все мои статьи - попытка объяснить самому себе как что-либо работает. В этот раз разбираемся с Nornir.

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

Сегодня в программе

Задавшись целью что-либо автоматизировать, перед тобой встанет вопрос “Что нужно, чтобы что-то автоматизировать?” Инструменты, конечно же, но какие?
В первом приближении, инструменты можно разделить на два типа:

  1. Язык программирования и библиотеки
  2. Инструменты управления конфигурацией (configuration management tools)

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

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

Кроме того, в мире Python существует множество библиотек, предназначенных для автоматизации различных задач. Нас интересуют в первую очередь те, что позволяют решать задачи сетевой автоматизации: netmiko, NAPALM, requests, nornir и многие другие.

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

Звучит, конечно, круто, но до такого уровня еще нужно доползти, но хочется же сразу взять и попробовать что-то автоматизировать, так ведь? Тут мы переходим ко второму типу инструментов - configuration management tools.

Chef, Puppet, Ansible, Salt - слова, которые я встретил еще при подготовке к CCNA. Из этого списка в реальности я встречал только Ansible. Чуть дальше мы о нем поговорим подробнее, потому что тема сравнения ansible vs nornir достаточно холиварная и лучше разобраться с этим на берегу.

Где-то посередине двух типов вклинился Nornir, о нем и поговорим.

Nornir

Разработку Nornir инициировал Дэвид Баррозо (David Barroso), создатель NAPALM, ещё в 2017 году. Через некоторое время проект оброс поддержкой сообщества и его разработкой занималась уже целая команда инженеров, среди которых Кирк Байерс (автор Netmiko), Патрик Огенстад, автор блога Networklore, Дмитрий Фиголь, на тот момент инженер Cisco и др.

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

Этот фреймворк предоставляет уровень абстракции для выполнения операций на сетевых устройствах, инвентаризации, вывода результатов и отладки. Он также имеет плагины, которые позволяют использовать инструменты, такие как Netmiko и NAPALM, для обращения к сетевому оборудованию и обработку входящей информации. В отличие от Ansible, Nornir не использует DSL (специализированный язык описания) и предоставляет возможность работы с чистым Python.

Nornir - 100% python, в отличии от Ansible, который использует DSL (domain-specific language)

Для работы с оборудованием Nornir использует inventory, это обычный набор файлов, в которых описаны хосты, группы и различные параметры. Кроме списка оборудования, необходимо иметь, говоря терминами Ansible, “playbook”, которым здесь является питоновский скрипт со всеми вытекающими удобствами и сложностями.

Nornir scheme

Структура Nornir следующая:

  • inventory - список хостов, описанных в файле, на которых применяются tasks
  • tasks - функция и она же задача, запускаемая на каждом хосте
  • functions - функции, которые работают глобально, например print_result, которая выводит результат задачи

Как начать работать с Nornir

Зачем вообще использовать Nornir, если можно просто писать на чистом Python? Почему бы не взять и написать все на Python? Ответ прост - абстракции. Nornir связывает данные из inventory с задачами (tasks), позволяя запускать эти задачи на определенном подмножестве устройств. Он обрабатывает данные, распараллеливает их выполнение, а также отслеживает возвращаемые результаты и ошибки.

Иными словами, это удобный инструмент, позволяющий оборачивать задачи к сетевому оборудованию в понятную структуру.

Начать работать с Nornir проще, чем кажется. Вот минимальный набор навыков, который нужно разобрать перед началом:

  1. Установить python на вашу ОС (предполагается один из вариантов Linux/WSL Windows)
  2. Разобраться с виртуальными окружениями в Python. Они позволят вам создавать изолированные среды с нужной версией Python и необходимыми пакетами для каждого проекта
  3. Иметь представление о переменных и структурах данных в Python, а также о логике работы и создании функций
  4. Что такое модули и как их импортировать в проект

Чтобы охватить все перечисленные пункты, достаточно будет осилить полторы главы из книги Python для сетевых инженеров за авторством Наташи Самойленко:

  • Глава I: полностью
  • Глава II: 9, 10, 11 темы

За подробными объяснениями можно обратиться к лекциям на youtube канале Наташи

Я рекомендую начать именно с этой книги т.к там есть ответы на большинство вопросов, с которыми вы столкнетесь на старте: какой редактор выбрать, как поставить python, почему лучше сразу осваивать Linux, основы Python и т.п.

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

С Nornir (да и вообще) удобнее работать через виртуальные окружения, в котором будет использоваться нужная версия python и необходимые пакеты под конкретный проект.

Все последующие команды в статье будут выполняться из виртуального окружения

Установим Nornir и несколько плагинов:

pip install nornir 
pip install nornir_utils 
pip install nornir_jinja2 
pip install nornir_netmiko

Вот так будет выглядеть стандартная структура проекта для Nornir. После установки, Nornir автоматически не создаст файлов и директорий, поэтому собираем сами:

. 
├── nr_config.yaml    # файл конфига Nornir
├── scripts    # директория для скриптов
├── inventory    # директория для файлов inventory
│ ├── hosts.yaml    # хосты + параметры
│ ├── groups.yaml    # группы + параметры
│ └── defaults.yaml    # все остальное параметры, не попавшие в hosts/groups

В некоторых статьях встречал иную структуру, в которой было разделение на задачи и исполняемые скрипты, по типу include_tasks в Ansible, но мы пока опустим этот момент.

Nornir предполагает создание директории Inventory, внутри которой будут расположены файлы с расширением .yaml и собственно скриптов автоматизации с расширением .py.

Inventory в Nornir обрабатывается с помощью плагина SimpleInventory, который используется по умолчанию. SimpleInventory - считывает все соответствующие данные из трех yaml-файлов: hosts, groups, defaults, которые нужно заполнить самостоятельно.

Для создания inventory существует не только SimpleInventory. Есть плагин, который, например, может взять список хостов из вашего netbox/nautobot автоматически, используя API или обработать инвентори Ansible

Разберем структуру каждого из файлов в директории Inventory.

hosts.yaml

В файле hosts.yaml указываются хосты, которые Nornir будет использовать в качестве “целевых хостов” (target host) для назначения задач.
Так выглядит мой файл hosts.yaml, более комплексный пример можно найти в документации:

---
r1:
  hostname: 192.168.1.26
  port: 2001
  groups:
    - ios_routers
...*вывод сокращен*
sw1:
  hostname: 192.168.1.26
  port: 2100
  groups:
    - ios_switches
...*вывод сокращен*

В данном примере у нас есть хосты с именами r1, r2, …, r17 и sw1, sw2, …, sw7. Для каждого хоста указаны IP-адрес и порт (в моей лабораторной среде доступ к хостам осуществляется через промежуточный хост (jump-host), где используется static NAT для перенаправления порта на соответствующий IP-адрес:порт). Также хосты объединены в группы.

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

groups.yaml

Файл groups.yaml позволяет организовать хосты в группы на основе их функциональности и других критериев. Формат файла groups.yaml аналогичен формату файла hosts.yaml в отношении форматирования и наследования, и также имеет возможность наследования элементов от других групп.

Вот пример содержимого файла groups.yaml:

---
global:
  data:
    domain: lab-nornir

ios_routers:
  platform: ios
  data:
    role: router

ios_switches:
  platform: ios
  data:
    role: switch

Тут у нас есть группа “global”, в которой определен атрибут “domain” со значением “lab-nornir”. И две отдельных руппы “ios_routers” и “ios_switches”, каждая из которых имеет атрибут “platform” со значением “ios” и атрибут “data” с дополнительной информацией. Например, для группы “ios_routers” определен атрибут “role” со значением “router”.

В коде плагина nornir_netmiko platform = device_type, поэтому если не указать platform, вы получите ошибку KeyError: 'device_type'

defaults.yaml

Данные в этом файле будут взяты для хоста в том случае, если в hosts или groups не будет специфических значений для указанного хоста. Структура файла идентична файлам hosts/groups:

---
username: cisco
password: cisco

Например тут я могу указать учетные данные для подключения. Если для конкретного хоста не указаны учетные данные, то будут использоваться значения из файла defaults.yaml.

Для работы с учетными данными лучше воспользоваться сторонними практиками, например, самое простое, подгружать их через модуль dotenv

Теперь перейдем к еще одной важной части Nornir - конфигурационный файл.

nr_config.yaml

При инициализации, Nornir в обязательном порядке запрашивает параметры работы, из проще всего описать в конфигурационном файле, например назовем его nr_config.yaml.

Конфиг состоит из двух секций inventory и runner, что в них есть?

Так выглядит стандартный конфигурационный файл:

inventory:
    plugin: SimpleInventory
    options:
        host_file: "inventory/hosts.yaml"
        group_file: "inventory/groups.yaml"
        defaults_file: "inventory/defaults.yaml"
runner:
    plugin: threaded
    options:
        num_workers: 20

В секции inventory мы указываем какой плагин инвентори использовать, затем в options рассказываем Nornir где лежат основные файлы инвентаризации hosts/groups/defaults.

Параметры файла специфичны для плагина инвентаризации, поэтому при использовании, например, nornir_netbox это будет URL и токен.

Секция runner отвечает за распараллеливании задач, так же определяется плагинами (по умолчанию threaded), количество потоков представлено в num_workers).

Если указать параметр plugin: serial для runner, то Nornir выполнит задачу на каждом хосте по очереди в простом цикле без какого-либо распараллеливания. При отладке распараллеливание рекомендуется отключать, установив num_workers: 1 т.к дебагер python не очень адекватно работает с потоками.

Подробнее про распараллеливание задач описано в документации Nornir - Execution Model. Я еще затрону эту тему чуть ниже, когда дойдем до группировки задач.

С конфигом и Inventory разобрались. В директории scripts мы будем создавать будущие скрипты, это просто удобнее, чем хранить все в корне проекта. Теперь можно переходить к созданию нашего скрипта, который будет отправлять show команды на сетевое оборудование и выводить результат в окно терминала.

Первый скрипт

Мы готовы разобрать первый скрипт Nornir. Рассмотрим на примере скрипта для опроса устройств Cisco и сбора вывода команд show clock и show version. Это простой скрипт, но он уже больше приближен к реальности, поэтому может показаться сложным. Сначала представлю сам скрипт, а дальше пойдем разбирать построчно:

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result
from dotenv import load_dotenv
import os


def show_commands(task):
    task.run(
            task=netmiko_send_command,
            command_string="show clock",
            name="show clock"
    )
    task.run(
            task=netmiko_send_command,
            command_string="show version",
            name="show version"
            )


if __name__ == "__main__":
    nr = InitNornir(config_file="nr_config.yaml")

    load_dotenv()
    nr.inventory.defaults.username = os.getenv("DEVICE_USERNAME")
    nr.inventory.defaults.password = os.getenv("DEVICE_PASSWORD")

    routers = nr.filter(role="router")

    results = routers.run(name="show commands", task=show_commands)
    print_result(results)

Если после объяснения работы скрипта останутся вопросы, смело задавайте в комментариях к посту в Telegram-канале.

Сначала разберемся в структуре скрипта, это поможет лучше понять логику его написания. Он состоит из нескольких блоков:

  • импорты модулей
  • основной блок с кодом, где обычно размещаются все функции (задачи), у нас тут как раз задача с сгруппированными подзадачами
  • блок if __name__ == "__main__", код внутри этого блока будет выполнен только при запуске скрипта напрямую

Глянем подробнее, что тут происходит и почему. Сейчас скрипт не такой большой, поэтому могу позволить себе объяснить каждую строчку в рамках статьи.

Импортируем модули

Сперва мы определяем, какие модули будут использованы в нашем скрипте и импортируем их конструкцией from модуль import объект, вот так будет выглядеть итоговый набор импортированных модулей:

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result

import os
from dotenv import load_dotenv

Это не единственный способ, импортировать модули можно по разному, целиком, отдельные функции, “прицепить” рядом со скриптом или импортировать под другим именем (для удобства)

Что тут что? Во-первых, мы импортируем основной класс Nornir:

from nornir import InitNornir

InitNornir будет создавать экземпляр класса, это основной объект, через который будут выполнятся все интересные вещи (например запуск выполнения задач). Этот объект делает Nornir тем, чем он является.

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

В 2020 году была открыта дискуссия по измению кода Nornir в PR#486 предлагалось оставить в коде фреймворка только ядро (его движок), а все плагины перевезти из ядра в отдельные репозитории. Т.е теперь, чтобы воспользоваться, например, netmiko, нужно будет его установить через pip.

Сейчас мы работаем с Nornir v3.3, на 07.2023 это последняя версия, где мы вольны устанавливать и импортировать те плагины, которые нам необходимы именно в такой логике. Существуют сетевые плагины для подключения к устройствам (nornir-napalm, etc), плагины для создания инвентаризации (nornir-netbox, etc) и nornir-utils, который содержит некоторые из бывших основных задач, таких как print_result и load_yaml.

Для доступа к устройствам будем использовать плагин nornir-netmiko, от него нам понадобится то, что и обычно делает netmiko, подключается к оборудованию и передает команды. Для вывода результата используем функцию get_result из nornir_utils. Импортируем их в следующих строчках:

from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result

Так же есть плагины и для других инструментов: Scrapli, NAPALM, Ansible, Jinja2, Netbox и т.п, полный список плагинов доступен на nornir.tech.

Для работы с кредами в скрипте используется dotenv модуль. Минимальный смысл его в том, чтобы не светить учетные данные в обязательных файлах Nornir, а держать их (а может и не только их) в специальном файле .env, который находится в корне проекта. Импортируем его в строчке:

from dotenv import load_dotenv

Если проект пушится в публичную репу (например github), то необходимо добавить файл .env в файл .gitignore, который регистрируется в корневом каталоге репозитория.

GIT игнорирует файлы прописанные в .gitignore т.е они не попадают в будущие коммиты. Это могут быть временные файлы .log/lock/tmp, кэши и в нашем случае .env

И импортируем оставшийся модуль os, который предоставляет функциональность для работы с операционной системой. Он будет использоваться позже для получения переменных среды. и dotenv - позволяет загружать переменные среды из файла .env.

Определяем задачи Nornir

Теперь определяем нашу основную задачу, которая представляет из себя обычную функцию python, в которую передается аргумент task:

def show_commands(task):
    task.run(
            task=netmiko_send_command,
            command_string="show clock",
            name="show clock"
    )
    task.run(
            task=netmiko_send_command,
            command_string="show version",
            name="show version"
            )

Я пока не очень уверенный сварщик, чтобы четко объяснить как Nornir работает внутри, поэтому поправляйте меня, если я где-то неправ.

Я не сразу понял, что можно обойтись без return в функции show_commands.

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

Изначально функция выглядела вот так:

def show_commands(task):
    resutl = task.run(
            task=netmiko_send_command,
            command_string="show clock",
            name="show clock"
    )
    return result

В данном случае, код продолжает работать и без использования оператора return в функции show_commands, потому что внутри функции выполняются задачи с помощью метода task.run(), и результаты этих задач сохраняются внутри объекта task, а и функция print_result использует эти результаты для вывода информации на экран.

Что из себя представляет объект task? Это объект, экземпляр класса Task который представляет собой задачу, которую нужно выполнить на устройстве.
Вот пример использования объекта task для получения информации о задаче:

print(task.name)  # Выводит имя команды задачи 
print(task.host)  # Выводит информацию об устройстве, на котором выполняется задача # и т.д.

Объекты task предоставляют удобный способ получения доступа к различным атрибутам и данным, связанным с задачей, то что это класс, мы можем увидеть через type(task):

<class 'nornir.core.task.Task'>

При желании, можно пойти в исходники и найти этот класс Task:

Nornir source code

Когда мы упоминаем класс (в нашем случае -Task), важно понимать, что это объект из концепции объектно-ориентированного программирования (ООП). ООП - это подход к программированию, основанный на концепции классов и объектов. Классы определяют структуру и поведение объектов, а объекты являются экземплярами этих классов. В случае с объектом task, он представляет собой экземпляр класса Task с определенными свойствами и методами.

Когда мы передаем объект task в функцию show_commands, мы можем использовать его для выполнения задач и получения информации о них. Результаты задач сохраняются внутри объекта task и могут быть использованы позже, например, при вызове функции print_result, чтобы вывести результаты выполнения задач на экран.

Внутренности name == “main

Возвращаемся к скрипту. С основным блоком кода закончили, переходим к конструкции if __name__ == "__main__".

Когда вы запускаете Python-скрипт напрямую, интерпретатор Python присваивает встроенной переменной __name__ значение "__main__". Если же файл импортируется как модуль, значение __name__ будет отличаться от "__main__" и будет соответствовать имени модуля.

Таким образом, выражение if __name__ == "__main__": позволяет определить, является ли файл главным исполняемым файлом, и выполнить соответствующий код только в этом случае. Это полезно, когда у вас есть код, который должен быть выполнен только при запуске файла напрямую, а не при его импорте в другой скрипт.

Почему используется именно такое выражение? Это стандартная практика в Python, ей обеспечивается модульность и гибкость при разработке программ. Вы можете определить различное поведение для запускаемого файла и импортированного модуля.

Зачем это нужно? Возможны несколько причин:

  1. Вы можете разместить код, который должен быть выполнен только при запуске файла, например, инициализацию переменных, установку настроек или вызов основной функции.
  2. Это позволяет вам проводить тестирование и отладку кода, запуская его напрямую, а не только импортируя его в другие модули.
  3. Это позволяет другим разработчикам импортировать ваш модуль без выполнения неожиданного кода. Код внутри блока if __name__ == "__main__": не будет выполнен при импорте модуля.

В текущем скрипте я поместил внутрь этого блока инициализацию nornir, фильтры, загрузку учетных данных для доступа к SSH и вызов основной задачи с печатью результата в терминал:

if __name__ == "__main__":
    nr = InitNornir(config_file="nr_config.yaml")

    load_dotenv()
    nr.inventory.defaults.username = os.getenv("DEVICE_USERNAME")
    nr.inventory.defaults.password = os.getenv("DEVICE_PASSWORD")

	routers = nr.filter(role="router")

    results = routers.run(name="show commands", task=show_commands)
    print_result(results)

Рассмотрим этот блок построчно. Создаем объект Nornir nr через вызов InitNornir и передаем в него конфиг, описанный в файле nr_config.yaml, который мы создавали чуть ранее. nr это основной экземпляр класса InitNornir, который мы будем использовать дальше:

nr = InitNornir(config_file="nr_config.yaml")

C помощью модуля dotenv через метод load_dotenv загружаем переменные окружения из файла .env. Затем передаем логин/пароль для устройств, которые у нас хранятся в файле .env в виде переменных, используя метод getenv, используя модуль os:

load_dotenv()
nr.inventory.defaults.username = os.getenv("DEVICE_USERNAME")
nr.inventory.defaults.password = os.getenv("DEVICE_PASSWORD")

Внутри файла .env, который находится в корне проекта, всего две строчки с переменными DEVICE_USERNAME и DEVICE_PASSWORD:

DEVICE_USERNAME=cisco
DEVICE_PASSWORD=cisco

Nornir умеет в различные фильтры, следующая строчка использует фильтр по роли. У нас есть две роли для устройств switch и router, которые мы описали в файле groups.yaml, запускаем задачу только для роли router. Тут мы создаем новую переменную, чтобы было понятно с чем мы дальше работаем:

routers = nr.filter(role="router")

В переменную results кладем результат выполнения задачи через метод run, который запустит задачу на всех хостах, указанных в inventory, одновременно. А параметром name задаем удобное имя, которое будет использовано в выводе конечного результата:

results = routers.run(name="show commands", task=show_commands)
print_result(results)

Вложенные задачи

Теперь мы можем немного поговорить о подзадачах и принципах задача-подзадача в Nornir. Распараллеливание включается в работу, когда вы запускаете задачу через nornir.core.Nornir.run с num_workers > 1.

При распараллеливании задач nornir будет использовать разные потоки для каждого хоста.

В документации есть две хорошие картинки, объясняющие полезность групповых задач:

Nornir sub task

Тут у нас есть основная задача (main task) с другими задачами внутри (sub task). Когда задачи являются вложенными, внутренние задачи будут выполняться последовательно для этого хоста параллельно с другими хостами. Это полезно, поскольку позволяет вам контролировать ход выполнения по своему усмотрению. Например, как показано в примере на картинке, можно разделить задачи по типам, собрать в группу один тип, а остальные пусть живут отдельно.

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

Параллельность задач реализуется через concurrent.futures. Ранее все работало через multiprocessing.dummy. С версии 2.4 эта задача переехала на плечи concurrent.futures, PR #467

В Python очень натянутые отношения с многопоточностью. В реализации CPython есть такая штука как GIL. Чтобы не раздувать статью еще больше, можно почитать об этом в книге Наташи.

Модуль concurrent.futures представляет собой эффективный способ использования параллельного выполнения задач, особенно в случаях, когда задачи блокируются на операциях ввода-вывода (I/O-bound). Если коротко, пока ждем ответа по одной задаче, можно впихнуть в этот промежуток выполнени следующей.

Закончим с параллельностью, надеюсь, я смог донести общий смысл. Вернемся к нашему скрипту. Все, что возвращает нам results, является объектом Nornir. Функция print_result как раз и занимается представлением данных внутри этого объекта в терминал.

Nornir отслеживает состояние задач, это пригодится нам в одном из скриптов.

Структура results

Чтобы понять, как вывести результаты, сначала нужно понять формат данных, возвращаемых Nornir. У Nornir есть такие структуры как AggregatedResultи MultiResult. При этом MultiResult вложена в AggregatedResult, чуть ниже это посмотрим.

Nornir предоставляет различные объекты, каждый из которых содержит атрибуты/данные результатов, например, отобразим дерево объектов для results - наш итоговый объект, в который собираются все результаты по всем хостам.

Удобнее это сделать в дебагере, например pdbr (одна из разновидностей pdb, в данном случае это (pdb+ rich)) и посмотрим на эти структуры поближе. Для начала устанавливаем pdbr:

pip install pdbr
python -m pdbr scripts/show_command.py

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

  • n (next) - выполнить все до следующей строки. Эта команда не заходит в функции, которые вызываются в строке
  • s (step) - выполнить текущую строку, остановиться как можно раньше. Эта команда заходит в функции, которые вызываются в строке
  • c (continue) - выполнить все до breakpoint. Также полезна, когда скрипт отрабатывает с исключением, позволяет дойти до строки, где возникло исключение

Методично используем n, пока не дойдем до конца скрипта:

pdbr example

На этом этапе мы уже можем смотреть сформированные переменные. Что хорошо в pdbr, есть, например, команда v, которая покажет список всех существующих переменных и их значения:

pdbr vars

Для наглядности воспользуемся связкой nornir-inspect иpdbr , предварительно импортировав сам модуль from nornir_inspect import nornir_inspect прямо внутри pdbr:

(Pdbr) from nornir_inspect import nornir_inspect
(Pdbr) nornir_inspect(results)
<class 'nornir.core.task.AggregatedResult'>
├── failed = False
├── failed_hosts = {}
├── name = show commands
├── <class 'nornir.core.task.MultiResult'> ['r1']
│   ├── failed = False
│   ├── failed_hosts = {}
│   ├── name = show commands
│   ├── <class 'nornir.core.task.Result'> [0]
│   │   ├── changed = False
│   │   ├── diff =
│   │   ├── exception = None
│   │   ├── failed = False
│   │   ├── host = r1
│   │   ├── name = show commands
│   │   ├── result = None
│   │   ├── severity_level = 20
│   │   ├── stderr = None
│   │   └── stdout = None
│   ├── <class 'nornir.core.task.Result'> [1]
│   │   ├── changed = False
│   │   ├── diff =
│   │   ├── exception = None
│   │   ├── failed = False
│   │   ├── host = r1
│   │   ├── name = show clock
│   │   ├── result = *13:33:50.823 UTC Sun Jul 9 2023
│   │   ├── severity_level = 20
│   │   ├── stderr = None
│   │   └── stdout = None
│   └── <class 'nornir.core.task.Result'> [2]
│       ├── changed = False
│       ├── diff =
│       ├── exception = None
│       ├── failed = False
│       ├── host = r1
│       ├── name = show version
│       ├── result = **вывод show version вырезан для удобства восприятия**
│       ├── severity_level = 20
│       ├── stderr = None
│       └── stdout = None

nornir-inspect отличный помощник, чтобы визуализировать дерево таких объектов и отследить, например, вложенность и понять как обратиться к тому или иному атрибуту.

Обратимся к results, чтобы понять, как Nornir предлагает нам работать с собранными данными:

(Pdbr) results
AggregatedResult (show commands): {'r1': MultiResult: [Result: "show commands", Result: "show clock", Result: "show version"], 'r2': MultiResult: [Result: "show commands", Result: "show clock", Result: "show version"], 'r3': MultiResult: [Result: "show
commands", Result: "show clock", Result: "show version"], 'r4': MultiResult: [Result: "show commands", Result: "show clock", Result: "show version"],...}

Чтобы узнать, для каких хостов собраны результаты, можно воспользоваться results.keys()

Вот он наш объект AggregatedResult - это словарь-подобный объект, который агрегирует результаты для всех устройств с хостами либо итерационно, либо вызывается по отдельности. А раз это словарь, вы можете просмотреть конкретный ключ хоста и увидеть что внутри:

(Pdbr) results["r1"]
MultiResult: [Result: "show commands", Result: "show clock", Result: "show version"]

Эта конструкция возвращает нам результат по отдельному хосту. Внутри это представлено объектом MultiResult. MultiResult ведет себя как список и охватывает, в том числе, случай, когда основная задача может иметь несколько подзадач, каждая из которых имеет свой собственный результат, как у нас сейчас. Мы можем это увидеть, запросив порядковый индекс:

(Pdbr) results["r1"][1]
Result: "show clock"
(Pdbr) results["r1"][2]
Result: "show version"

Это, к слову, и есть две наших подзадачи. Индекс [0] - всегда будет основная задача - show commands. Наконец, чтобы посмотреть результат для конкретной задачи, воспользуемся атрибутом .result:

(Pdbr) results["r1"][1].result
'*05:18:00.141 UTC Sun Jul 9 2023'
(Pdbr) results["r1"][2].result
'Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)\nTechnical Support: http://www.cisco.com/techsupport\nCopyright (c) 1986-2016 by Cisco Systems, Inc.\nCompiled Tue 22-Mar-16 16:19 by
prod_rel_team\n\n\nROM: Bootstrap program is IOSv\n\nR1 uptime is 1 day, 9 hours, 46 minutes\nSystem returned to ROM by reload\nSystem image file is "flash0:/vios-adventerprisek9-m"\nLast reload reason: Unknown reason\n\n\n\nThis product contains
cryptographic features and is subject to United\nStates and local country laws governing import, export, transfer and\nuse. Delivery of Cisco cryptographic products does not imply\nthird-party authority to import, export, distribute or use
encryption.\nImporters, exporters, distributors and users are responsible for\ncompliance with U.S. and local country laws. By using this product you\nagree to comply with applicable laws and regulations. If you are unable\nto comply with U.S. and
local laws, return this product immediately.\n\nA summary of U.S. laws governing Cisco cryptographic products may be found at:\nhttp://www.cisco.com/wwl/export/crypto/tool/stqrg.html\n\nIf you require further assistance please contact us by sending
email to\nexport@cisco.com.\n\nCisco IOSv (revision 1.0) with  with 984313K/62464K bytes of memory.\nProcessor board ID 9FQ1XC4GCTS2FO3RXG6NC\n4 Gigabit Ethernet interfaces\nDRAM configuration is 72 bits wide with parity disabled.\n256K bytes of
non-volatile configuration memory.\n2097152K bytes of ATA System CompactFlash 0 (Read/Write)\n0K bytes of ATA CompactFlash 1 (Read/Write)\n0K bytes of ATA CompactFlash 2 (Read/Write)\n0K bytes of ATA CompactFlash 3 (Read/Write)\n\n\n\nConfiguration
register is 0x0\n'

Собственно ничего сверхъестественного, видим то, что вернули нам сетевые коробки.

Более того, в pdbr мы можем воспользоваться inspect и посмотреть, какие атрибуты доступны, кроме result:

pdbr inspect

Например, кроме name - названия задачи, мы можем использовать атрибут failed. который позволит нам увидеть, была ли задача успешно выполнена или нет:

(Pdbr) results["r1"].failed
False    # если задача будет неуспешна = True

Шаги выше мы провернули для того, чтобы увидеть результат задачи для одного хоста. Трудоемко вышло. В скрипте используется print_result который все сделает за нас, вот что получилось в итоге:

$ python scripts/show_command_task.py
* r9 ** changed : False ********************************************************
vvvv show commands ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- show clock ** changed : False --------------------------------------------- INFO
*05:17:38.731 UTC Sun Jul 9 2023
---- show version ** changed : False ------------------------------------------- INFO
Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(2)T, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2016 by Cisco Systems, Inc.
Compiled Tue 22-Mar-16 16:19 by prod_rel_team


ROM: Bootstrap program is IOSv

R9 uptime is 1 day, 9 hours, 39 minutes
System returned to ROM by reload
System image file is "flash0:/vios-adventerprisek9-m"
Last reload reason: Unknown reason



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

Cisco IOSv (revision 1.0) with  with 984313K/62464K bytes of memory.
Processor board ID 9QFX3RQDPOTR9AAGQ8YDA
4 Gigabit Ethernet interfaces
DRAM configuration is 72 bits wide with parity disabled.
256K bytes of non-volatile configuration memory.
2097152K bytes of ATA System CompactFlash 0 (Read/Write)
0K bytes of ATA CompactFlash 1 (Read/Write)
0K bytes of ATA CompactFlash 2 (Read/Write)
0K bytes of ATA CompactFlash 3 (Read/Write)



Configuration register is 0x0

^^^^ END show commands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...*вывод сокращен*

Красивый вывод с разделениям на хосты и задачи, посмотрим подробнее, что мы тут видим:

* r9 ** changed : False ********************************************************
vvvv show commands ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
---- show clock ** changed : False --------------------------------------------- INFO
*05:17:38.731 UTC Sun Jul 9 2023
...*вывод сокращен*
^^^^ END show commands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

В первой строчке обозначается хост, на котором исполняется задача. Тут же отображается состояние changed : False/True, это не актуально для команд show и будет использовано в конфигурационных задачах.

Ниже через символьные разделители отмечена основная задача и подзадача с ее результатом, а в конце видим блок END, подсказывающий о звершении задачи.

На этом с базовым разбором Nornir мы закончим, если появятся вопросы, я статью дополню, либо учту в будущих статьях. Осталось разобрать вопрос, а почему, собственно Nornir?

Почему не Ansible?

В заключении хочу затронуть тему сравнения Nornir и Ansible. Вопрос дискуссионный и, на мой взгляд, любой инструмент удобнее рассматривать в контексте конкретной задачи и сложившихся условий, но у вопроса есть ряд объективных критериев, которые все же можно подвергнуть сравнению, этим и займемся.

Если вы еще не в курсе что такое Ansible и какое место он занимает в сетевой автоматизации, рекомендую сначала набросать себе общую картину автоматизации сетей в целом, либо просто попробовать этот инструмент на практике.

Коротко: в мире управления сетевыми устройствами существует два широко известных инструмента: Ansible и Nornir. Это один из холиваров на тему “а что лучше”. Спойлер: зависит от задачи, стартовых условий, желания команды развивать скиллы в ту или иную сторону и т.п.

Что такое Ansible?

Ansible - это open-source фреймворк без агента, разработанный компанией Red Hat для автоматизации управления хостами через SSH.

Ansible scheme

Структура ansible выглядит следующим образом:

  • inventory - список хостов, так же представляет из себя отдельный файл
  • playbook - набор действий, которые мы желаем применить к хостам из inventory. Внутри плейбуков идет разделение на:
    • play - некоторая задача, внутри которой может выполняться несколько task
    • task - задачи в рамках определенного play
      • module - в рамках task могут быть запущенны различные модули

Inventory и playbook - файлы на основе YAML.

Ansible берет на вход для подключения данные из inventory, подключается к ним по SSH (Ansible не требует установки агента на удаленный хост, т.е является agentless) и выполняет действия, описанные в playbook.

Инструменты похожи, но тут важны детали. Почему я предлагаю остановиться именно на Nornir? Однозначного ответа нет, поэтому я постараюсь раскрыть мысль, почему в моем случае Nornir оказался более подходящим решением.

Nornir vs Ansible

Чистый Python

По мере усложнения логики и цикла жизни ваших плейбуков их сложность может значительно вырасти т.к все “хотелки” придется писать в рамках логики DSL. В Nornir же чистый python. Это полезно т.к вы параллельно учите и python, который сможете применять в других задачах, где Nornir не обязателен.

Апдейты не ломают скрипты

Так же частая проблема - обновления Ansible, которые могут поломать вам плейбуки/модули и придется потратить время, чтобы разобраться в изменениях.
В Nornir таких проблем сильно меньше. Из последнего, например, на что наткнулся я, вынесли логику модулей из ядра в отдельные плагины и в большинстве туториалов (старше 2020 года) вы наткнетесь на устаревшие import. Проект растет и его все сложнее поддерживать, поэтому было принято решение поставлять лишь core движок nornir, а плагины вынести в отдельные репы.
Т.е чтобы починить скрипт, нужно было сделать несколько действий:

  1. Установить нужные плагины через pip pip install nornir-netmiko etc.
  2. Поправить импорты в скрипте

Дебаг

Про дебаг и так все понятно. Дебажить придется много, а в Nornir это делать удобнее т.к используются стандартные тулзы python - [pdb(https://docs.python.org/3/library/pdb.html)]/rich или его разновидности, например pdbr (pdb + rich)
В ансибле все это в рамках дебаг модулей, что усложняет дебаг как таковой.

Скорость

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

За многопоточность в Nornir отвечает модуль concurrent.futures

Но есть нюанс…

Вроде преимуществ Nornir хватает, но есть нюанс… Ансибл прост для старта в автоматизации, поэтому Nornir не так часто встречается в гайдах по автоматизации чего-либо, так же не часто его встретишь и в конторах, где уже есть культура автоматизации. (судя по рассказам коллег, даже в крупных).

Порог вхождения в Ansbile проще т.к достаточно понимать YAML, синтаксис написания плейбуков и почитать документацию по нужным модулям. Классическая схема Easy to learn. Hard to master. Nornir тут в отстающих т.к придется начать разбираться в Python. С другой стороны, с питоном все равно придется сталкиваться, если вы встанете на путь автоматизации, поэтому решать вам.

Ansible предоставляет идемпотентность из коробки, что дает возможность без проблем запускать плейбуки, не думая, например: “а есть ли уже такая настройка на устройстве?” если ансибл найдет подобные строчки в конфиге - таск на добавление конфига будет пропущен для этой коробки.

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

Как итог, нужно понимать как работать с ансиблом, как минимум, это еще один инструмент в копилку для решения мириад проблем, которые автоматизация обязательно подкинет. Так же это хороший инструмент с хорошей поддержкой как со стороны сообщества, так и со стороны Red Hat.

Я выбрал Nornir потому, что это чистый python. Разобраться в инструменте и параллельно использовать питон, круто же? Круто! Ансиблом я писал плейбуки для обновления RouterOS, было криво-косо, но задачу я решил, ничего плохого сказать не могу т.к проблемы начинаются, по рассказам коллег, в количествах от 500+ устройств.

На этом пока остановимся, далее в планах рассмотреть рабочий сценарий на примере backup скрипта, сначала на Cisco, затем затронуть мультивендор, попробуем использовать фильтры, деление на подзадачи, потыкаем известные плагины, наконец доберусь до netbox лабы, автоматизации инвентори и т.п. Должно быть интересно. Спасибо что дочитали до конца.

Источники


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

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