Предисловие

Привет, Хабр!

Зачем я решил написать эту статью? Ведь тема-то не очень новая, в сети можно найти и документацию, и практические примеры. Тем не менее, когда я сам начал пытаться разобраться в этой теме, то мне не хватило эдакой all-in-one статьи, где хотя бы поверхностно пробежались по всем ключевым темам, позволяя читателю сформировать пусть и не максимально детальное, но целостное понимание о предмете: что это такое, зачем и как использовать. В процессе погружения в тему я оставил разные артефакты: заметки, примеры кода и т. п., которые могли бы послужить основой для подобного материала. И тогда я решил, что попробую написать статью именно так: не совершая слишком глубокого погружения в детали, охватить всю базу, необходимую для старта. После внимательного прочтения читатель с нуля сумеет на практике реализовывать взаимодействие с сетевым оборудованием через Netconf. А детальнее изучить каждую из тем можно факультативно — благо, по каждой отдельной теме информация в сети имеется.


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

Зачем нам все это?

Когда-то я, вооруженный Python и Netmiko, спокойно пилил скрипты для автоматизации рутины и, в общем, на жизнь не жаловался. Ведь наловчившись с регулярками и всякими expect, можно делать в CLI что угодно. Но загадочные слова netconf, yang, rpc манили, как что-то прогрессивное и устремленное в будущее, поэтому мне было неспокойно. Казалось, что я упускаю нечто важное в этой сфере. В какой-то момент терпеть стало невыносимо, и я вбил в поиск: «How to Netconf».

Если отбросить желание применить технологию только из-за самого факта ее существования, то что нам может дать все это, и зачем отказываться от CLI? Дело в том, что командная строка — интерфейс человек-машина. Вывод в CLI формируется и форматируется в виде, удобном для понимания человеком, но машина — дело другое. Чем легче человеку, тем получается сложнее понимать все это машине. Сложно, но можно. Вот только есть беда: вывод в консоль может внезапно после обновления ПО измениться в каких-то деталях, которые убьют имеющиеся regex и заставят их переписывать. В мире Huawei я с этим сталкивался. Ну а дальше все еще хуже: мало того, что у разных вендоров консоль отличается вплоть до полной неузнаваемости, так еще и в пределах одного вендора у разных линеек устройств может быть разное ПО с заметно отличающимся CLI (опять привет, Хуа). Все это практичности и универсальности не добавляет. Нужно адаптировать скрипты и следить за изменениями.

Как нам поможет Netconf? В первую очередь, он дает структурированный обмен данными, так как это интерфейс машина-машина. Это отменяет необходимость подолгу корпеть над регулярками, поскольку теперь мы обращаемся не с сырым текстом, а со словарями и списками  (после обработки полученного XML). Это, пожалуй, главное, из-за чего я захотел это все освоить. Но не единственное. Бонусами идут: унификация данных в пределах вендора и даже, местами, кроссвендорность. Иначе говоря, используя один скрипт в неизменном виде, мы можем работать не только с разными устройствами одного вендора, но и с устройствами разных вендоров! Звучит круто. Как все это реализуется, мы рассмотрим по порядку далее в этой статье.

Минусы тоже есть, надо заметить. Для меня лично оказался довольно неожиданно высоким порог вхождения, да и после освоения иногда приходится поломать голову над реализацией чего-либо. Второй значительный минус — неполный охват всех данных, имеющихся на устройстве. То есть, не все данные можно получить через Netconf. Здесь, правда, все зависит от реализации конкретного вендора, надо думать. И, как я слышал, многие отказываются от Netconf в пользу старого доброго CLI именно по этой причине. Быть или не быть — решать, конечно, вам.

Netconf

Сам протокол концептуально устроен не очень сложно. Он дает нам 2 главные возможности: получение какой-либо информации с сервера или изменение конфигурации на нем же. Под сервером нужно понимать, в стандартном случае, сетевое устройство, конечно же. Он реализует концепцию удаленного вызова процедур (RPC) и в качестве транспорта использует разные существующие протоколы. Чаще всего встречается SSH, так как его обязана поддерживать любая имплементация. В общем виде работает это следующим образом. Клиент должен сформировать XML документ, описывающий запрос для сервера. Далее он подключается к серверу через SSH, устанавливает сессию Netconf, в процессе чего он обменивается с сервером наборами capabilities — это такое описание поддерживаемых функций (и модулей) в контексте взаимодействия через Netconf. В случае, если все требуемые функции поддерживаются, клиент передает запрос в формате XML, сервер его обрабатывает и отправляет клиенту в ответ также XML с информацией об обработке запроса. Это может быть как просто сообщение типа <ok> (запрос успешно выполнен), так и запрошенные данные в виде XML-дерева или сообщение об ошибке с подробностями. Запрос содержит одну или несколько поддерживаемых операций с их атрибутами и набор необходимых данных.

Какие возможные операции предусмотрены? Их несколько изначально и еще они могут добавляться различными capabilities. Не хочу пытаться в одной статье охватить все, поэтому мы рассмотрим самые основные, которые необходимы для получения информации и изменения настроек:

  • get - получить информацию состояния (running)

  • get-config – получить конфигурацию

  • edit-config – изменить конфигурацию

  • copy-config - скопировать конфигурацию (между хранилищами)

Вот так выглядит базовый шаблон XML документа, который передается в сторону сервера:

<?xml version="1.0" encoding="utf-8"?> #Всегда будет идти в начале документа, обозначая версию XML и кодировку (последняя всегда должна быть UTF-8, иначе сервер вернет ошибку)
<rpc message-id=”101” xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> #Открывает секцию RPC и указывает message id, ответ от сервера на этот запрос будет содержать такой же message id. Про namespace (атрибут “xmlns”) мы поговорим позднее.
Внутри rpc расположен сам запрос и данные
</rpc> 
]]>]]> #обозначает конец сообщения

Примечание. Здесь нужна некоторая ретроспектива. В 2011 году вышел обновленный стандарт Netconf, который стал считаться версией 1.1. Сразу же вышло и обновление стандарта "Netconf over SSH", в котором end-of-message sequence ]]>]]> решили заменить на систему chunked framing. Ее смысл в том, что сообщение делится на "куски", а перед каждым куском сообщается его размер в байтах. Так принимающая сторона может точно знать, когда сообщение кончается. Поэтому, если оба — клиент и сервер поддерживают версию 1.1, они будут использовать логику chunked framing вместо end-of-message sequence.

Уже внутри тега <rpc> мы должны разместить всю информацию запроса. В первую очередь, указать операцию. Например, <get-config>. Поскольку Netconf поддерживает работу с разными хранилищами конфигурации, внутри этого тега мы должны также указать, из какого хранилища взять данные. Изначально обязательным является наличие только running конфигурации, наличие других хранилищ уже определяется поддержкой различных capabilities. Например, может быть хранилище candidate.

Вот пример полного сообщения, на которое сервер нам выведет полную "running" конфигурацию устройства:

<?xml version="1.0" encoding="utf-8"?>
<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="827">
  <get-config>
    <source>
      <running/>
    </source>
  </get-config>
</rpc>
]]>]]>

В ответ получим многостраничный документ с полной конфигурацией устройства. Но нужны ли нам все-все данные? Одна из важных функций — возможность фильтрации контента еще на стороне сервера. Включив внутрь <get-config> элемент <filter> с необходимой информацией, мы можем сказать серверу, какой конкретно кусок конфигурации мы хотим получить, и сервер вернет только его. Удобно! С этим мы и поупражняемся далее.

Инструментарий

С Netconf познакомились, а что еще нам нужно? Будем использовать Python и модуль, который возьмет на себя заботы об организации взаимодействия с сервером.

Мне на сегодня известно 3 модуля для python, работающих с Netconf:

  • ncclient - самый широко используемый, можно сказать, дефолтный.

  • netconf-client – альтернативный модуль, разработчик акцентируется на компактности

  • scrapli_netconf – модуль от разработчика scrapli

Я лично для себя выбрал последний. Вообще, кардинальных отличий в функциональности нет, и все они предоставляют почти одинаковый функционал, однако ncclient предлагает пару дополнительных инструментов для абстракции, включив поддержку оборудования разных вендоров. Но я хотел что-то максимально простое и с минимальной абстракцией от «чистого» Netconf. В конце концов, обладая некоторым знанием python, нужные штуки можно и дописать самому.

Репозиторий проекта и документация

Основное, что нам требуется от модуля — создание сессии Netconf, в которой мы будем отправлять запросы и получать ответы. Кроме этого, модуль еще помогает нам "конструировать" запрос, добавляя за нас базовые части XML. Но можно и работать в сыром режиме — когда мы сами должны описать полный XML запроса.

Устанавливаем соединение

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

Не забываем для начала установить модуль:

pip3 install scrapli_netconf

И создаем наш первый скрипт:

from scrapli_netconf.driver import NetconfDriver

switch = {
    "host": "192.168.0.1",
    "auth_username": "scrapli",
    "auth_password": "scrapli",
    "auth_strict_key": False,
    "port": 22
}

connection = NetconfDriver(**switch)
connection.open()
print(connection.netconf_version)
connection.close()

Если подключение успешно, то мы получим версию Netconf (1 или 1.1), которая была согласована клиентом и сервером.

А давайте теперь попробуем получить список capabilities, которые сообщил нам сервер, добавив пару строк между open и close:

for i in connection.server_capabilities:
  print(i)

В ответ получим много строк. Каждая capability имеет имя, представляющая собой URI.

Например, вот эти обозначают, что сервер поддерживает netconf 1.0 и 1.1:

urn:ietf:params:netconf:base:1.0

urn:ietf:params:netconf:base:1.1

А вот эти — что сервер поддерживает изменение непосредственно running конфигурации и также поддерживает хранилище конфигурации candidate:

urn:ietf:params:netconf:capability:writable-running:1.0

urn:ietf:params:netconf:capability:candidate:1.0

А вот уже пример чисто вендорский. Данное устройство поддерживает yang модуль huawei-ifm, причем указана конкретная ревизия модуля:

urn:huawei:yang:huawei-ifm?module=huawei-ifm&revision=2022-08-06&deviations=huawei-ifm-deviations-CE6866

Вот это последнее нам и понадобится в продолжении практики. А чтобы к ней перейти, нам нужно ввести еще пару понятий — yang и XML namespace.

YANG

Помните, я говорил, что Netconf сам по себе не определяет модель данных для описания конфигураций? Вот поэтому и придумали YANG.

Вкратце, YANG — это как раз и есть язык для описания модели данных. То есть, используя средства языка, можно описать XML дерево, которое содержит какую-то часть конфигурации. Вообще, возможности языка шире, чем просто описание имен элементов и их иерархии. Он позволяет, к примеру, описать типы переменных и их возможные значения. Мы тут остановимся только на том, что нам важно понимать в контексте поставленных задач.

Основная сущность — это модуль, который описывает какую-то конкретную часть конфигурации по функциональному признаку. Так, huawei-ifm — это модуль, который описывает структуру, имена тегов и иерархию объектов в XML документе, содержащем информацию о конфигурации интерфейсов. В чем прелесть: если мы в списке capability устройства видим имя этого модуля — это значит, что мы можем работать со знакомой нам структурой, не обращая внимания на модель оборудования и версию ПО (однако, обращая внимание на версию самого модуля). Понятно, что модуль huawei-ifm навряд ли будет поддерживаться коммутатором Cisco или Arista, но внутри линеек Huawei это работает.

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

Модуль представляет из себя текстовый документ с метаданными и непосредственно описания модели данных. Где найти эти самые модули и посмотреть их? Есть специальные ресурсы в сети. Например, Yang Catalog.

Заходим на страницу и вбиваем название модуля, например, huawei-ifm и нажимаем Get details. Здесь мы можем как скачать файл модуля, так и посмотреть разную информацию, а еще есть режим отображения дерева (Tree View) – удобный способ найти нужные объекты в человеко-понятном представлении.

 Еще один ресурс, предоставляющий аналогичный функционал: Netconf Central.

XML Namespace

Вообще, это понятие относится к XML, а не к YANG. Но, поскольку все это у нас работает совместно, не криминалом будет обсудить это в контексте YANG.

Namespace – это пространство имен. Проще всего понять это как контекст, в котором существуют элементы дерева XML. Дело в том, что в XML могут возникать конфликты имен, если имена тегов дублируются. Например, если в дереве есть 2 элемента с именем <name> на одном уровне иерархии, то мы, обращаясь по этому имени, не даем системе однозначной инструкции, к какому конкретно элементу мы обращаемся. Для этого и были введены namespace, или пространства имен. Они позволяют отделить мух от котлет, указать, к какому конкретно набору данных мы обращаемся.

В примере выше мы видели такой аргумент как  xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" — вот это и есть указание на namespace внутри тега, открывающего контейнер.

Вот в этом примере у нас возникнет проблема конфликта имен, сервер не поймет, какие данные мы запрашиваем, если обращаемся к контейнеру table.

<table>
  <address>192.168.0.1</address>
  <mask>255.255.255.0</mask>
</table>
<table>
  <city>Moscow</city>
  <country>Russia</country>
</table>

 А вот такой вполне будет работать, поскольку два контейнера относятся к разным namespace:

<table xmlns=”ipaddr”>
  <address>192.168.0.1</address>
  <mask>255.255.255.0</mask>
</table>
<table xmlns=”geo”>
  <city>Moscow</city>
  <country>Russia</country>
</table>

Обращаясь к контейнеру <table>, мы должны указать namespace - так система точно поймет, какие данные нас интересуют.

Augmentation

А это не что иное, как расширение. Представим, что разработчик А написал модуль, который описывает модель данных какой-либо конфигурации. Разработчик В видит, что в этом модуле отсутствует нужная ему ветка. Он мог бы взять весь модуль и вставить туда нужное ему добавление. Но ему нужно будет согласовывать это с разработчиком модуля А, и делать это каждый раз, если он захочет внести изменения. Или же он мог бы сделать форк и дописать модуль под свои нужды. Но тогда дальнейшая разработка всего модуля уйдет в сторону, нужно будет следить за оригинальным модулем, и все нововведения переносить в новый.

И то и другое не выглядит удобным. Поэтому в YANG есть такой механизм, как расширение модуля. Как это работает? Разработчик В пишет свою ветку в виде отдельного модуля и указывает, что эта ветка должна быть встроена в иерархию модуля разработчика А в определенном месте. Приведем пример.

Модуль А описывает такую структуру:

<system xmlns=”this:is:module:a”>
  <users>
    <user>
      <name/>
    </user>
  </users>
  <services>
    <service>
      <name/>
      <status/>
      <port/>
    </service>
  </services>
</system>

Разработчик модуля B хочет добавить внутрь <system> еще такой функционал:

<dns>
  <servers>
    <server>
      <name/>
      <address/>
    </server>
  </servers>
</dns>

Он пишет отдельный модуль "this:is:module:b", но указывает, что этот модуль расширяет (augments) модуль "this:is:module:a" в месте иерархии по указанному пути: /system/

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

<system xmlns=”this:is:module:a”>
  <users>
    <user>
      <name/>
    </user>
  </users>
  <services>
    <service>
      <name/>
      <status/>
      <port/>
    </service>
  </services>
  <dns xmlns=”this:is:module:b”>
    <servers>
      <server>
        <name/>
        <address/>
      </server>
    </servers>
  </dns>
</system>

Обращаясь к элементам этой добавленной ветки, мы должны указывать namespace расширяющего модуля, так как эти элементы существуют только его пределах. Namespace, заданный как аргумент в каком-либо объекте, действует на все вложенное дерево, пока в каком-либо вложенном объекте не задан другой namespace.

Когда мы будем далее рассматривать примеры запросов, будет понятнее, как это работает на практике.

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

Получение конфигурации

Итак, мы достаточно вооружились теорией, чтобы начать делать что-то полезное. Давайте попробуем для начала получить с устройства полную конфигурацию. Это очень просто сделать с помощью модуля scrapli_netconf, так как он умеет конструировать за нас самые стандартные запросы.

from scrapli_netconf.driver import NetconfDriver

switch = {
    "host": "192.168.0.1",
    "auth_username": "scrapli",
    "auth_password": "scrapli",
    "auth_strict_key": False,
    "port": 22
}

connection = NetconfDriver(**switch)
connection.open()

result = connection.get_config(source='running')

connection.close()

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

<?xml version="1.0" encoding="utf-8"?>
<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="827">
  <get-config>
    <source>
      <running/>
    </source>
  </get-config>
</rpc>

А в качестве указания на datastore конфига, в данном случае <running/>, он подставит значение из аргумента, который мы скормили модулю (source='running').

А что делать, если нам не нужен весь конфиг, а нужна конкретная часть? Хорошо, что в Netconf предусмотрен способ фильтрации данных на стороне сервера.

Фильтр данных

Вот здесь нам и пригодятся полученные ранее знания обо всех этих namespace и модулях. Давайте предположим, что мы хотим получить конфигурацию одного отдельно взятого интерфейса, пусть будет 25GE1/0/1.

from scrapli_netconf.driver import NetconfDriver

switch = {
    "host": "192.168.0.1",
    "auth_username": "scrapli",
    "auth_password": "scrapli",
    "auth_strict_key": False,
    "port": 22
}

FILTER = """
<ifm xmlns="urn:huawei:yang:huawei-ifm">
  <interfaces>
    <interface>
      <name>25GE1/0/1</name>
    </interface>
  </interfaces>
</ifm>
"""
 
connection = NetconfDriver(**switch)
connection.open()

result = connection.get_config(filter_=FILTER, source='running')

connection.close()

Мы написали часть XML запроса, которая будет обернута в контейнер <filter> и вставлена в запрос в нужном месте.

Полный RPC запрос выглядел бы так:

<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="827">
  <get-config>
    <source>
      <running/>
    </source>
    <filter type="subtree">
      <ifm xmlns="urn:huawei:yang:huawei-ifm">
        <interfaces>
          <interface>
            <name>25GE1/0/1</name>
          </interface>
        </interfaces>
      </ifm>
    </filter>
  </get-config>
</rpc>

Обратите внимание на атрибут элемента filter - type=”subtree”. Дело в том, что помимо фильтра в режиме дерева элементов, есть еще фильтр XPATH, напоминающий URL, но это мы в рамках данной статьи рассматривать не будем.

Разберем написанный нами фильтр. Поскольку режим у нас subtree, мы должны предоставить внутри контейнера filter дерево элементов ровно в том виде, в котором его бы нам вернул сервер, и этот вид, точнее модель, описана в соответствующем модуле urn:huawei:yang:huawei-ifm. Здесь мы начинаем с контейнера <ifm>, который является верхним уровнем иерархии этого модуля, при этом обязательно указываем имя модуля в качестве названия namespace. Внутри идет список <interfaces>. Список он потому, что может содержать внутри множество одинаковых контейнеров <interface> (соответственно кол-ву интерфейсов на устройстве). Поскольку нужно как-то отличать внутри этого списка разные экземпляры, необходим ключ этого списка. В данном случае ключом является название интерфейса, то есть элемент <name>. Он является также leaf-node, то есть он содержит значение и не может содержать вложенных элементов. Чтобы получить конфиг именно конкретного интерфейса, мы и обозначаем значение ключа. При таком фильтре мы получим самую полную конфигурацию отдельно взятого интерфейса 25GE1/0/1.

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

<interface>
  <name>25GE1/0/1</name>
  <description/>
</interface>

В данном случае вернется только описание. Обратите внимание на расположение слэша у leaf-node - оно справа, а не слева, как у закрывающего тега. Это сокращенная версия, а можно и полностью указать <description></description> - работать будет и так и так.

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

<interface>
  <description/>
</interface>

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

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

Разумеется, мы можем искать не только по ключу, но и по значению других элементов:

<interface>
  <description>to_server</description>
</interface>

В таком случае нам вернется конфиг интерфейса с таким описанием, а если такое описание на нескольких интерфейсах, то конфиг всех них.

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

Таким образом, фильтрация совмещает 2 подхода: поиск данных по совпадению значений элементов и уточнение перечня запрашиваемых данных.

Надеюсь, более-менее понятно объяснил. Для полноты теоретической подготовки нужно рассмотреть еще ситуацию с расширением модуля. Как это выглядит на практике и как с этим жить.

Если мы запросили бы полный вывод конфигурации интерфейса 25GE1/0/1, то внутри контейнера <interface> был бы в числе прочей информации такой контейнер:

<lacp-force-up xmlns="urn:huawei:yang:huawei-lacp">
  <force-up>true</force-up>
  <extension-enable>false</extension-enable>
</lacp-force-up>

У этого элемента указан namespace urn:huawei:yang:huawei-lacp. И он отличается от базового, который мы указывали в контейнере <ifm>. Это из-за того, что этот вот контейнер здесь является "пришельцем" из другого модуля, который называется huawei-lacp. Для нас это означает следующие вещи:

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

  • Если мы хотим обратиться адресно к элементу из встроенной ветки, необходимо указать его namespace.

Пример второго варианта:

<ifm xmlns=”urn:huawei:yang:huawei-ifm»>
  <interfaces>
    <interface>
      <name>25GE1/0/1</name>
      <lacp-force-up xmlns=”urn:huawei:yang:huawei-lacp”>
        <force-up/>
      </lacp-force-up>
    </interface>
  </interfaces>
</ifm>

Мы хотели узнать значение параметра <force-up> для интерфейса 25GE1/0/1. Пришлось указать namespace. Если его не указать, то сервер вернет ошибку bad element.

Изменение конфигурации

Вот и самое интересное! Зная, как формируются запросы и ответы, структуру данных, как работать с модулями, мы можем теперь что-то и изменить.

На самом деле, изменение конфигурации почти не отличается от чтения. Во всяком случае, структура данных абсолютно такая же, но сам запрос обрастает некоторыми дополнительными режимами. Суть заключается в том, чтобы передать устройству конфигурацию в виде того же XML дерева, а он ее прочитал и применил. Но есть нюанс. Например, что будет делать netconf-сервер, получив конфигурацию, которая конфликтует с уже имеющейся?

На это есть несколько типов операций:

  • merge — объединение новых данных с существующими. То есть, старое не удаляется, а дополняется новыми объектами, но если конкретный объект в конфигурации может быть только в единичном кол-ве, он будет заменен на новый. Например, BGP нейбор будет создан в дополнение к старым. А hostname будет заменен, так как 2 экземпляра на одном девайсе быть не может. Эта операция выполняется по умолчанию, если не указано иное.

  • replace – заменяет данные в соответствующем блоке на предоставленные в запросе.

  • сreate – создает конфигурацию только в случае, если ее пока нет. Если она есть, будет возвращена ошибка, то есть не будет произведена замена или объединение.

  • delete – удаляет указанные данные. Если данных и так не было, будет возвращена ошибка.

  • remove – удаляет данные, если они есть. В отличие от delete, не возвращает ошибок, если данных не было.

Операция указывается как аргумент в любом элементе, начиная с <config>, и она будет применена как к этому элементу, так и ко вложенным, пока каком-то из вложенных элементов не будет определена другая операция. Это дает гибкость: для разных кусков конфигурации можно выполнять обработку в разных режимах. Надо отметить, что этот атрибут существует в namespace urn:ietf:params:xml:ns:netconf:base:1.0, поэтому при указании операции нужно ссылаться на этот NS.

Кроме того, можно указать глобально для всего запроса параметр default-operation. По умолчанию это merge, глобально можно поменять поведение на replace или none. Последнее приведет к тому, что по умолчанию не будет выполняться никакая операция с данными, если она явно не указана в запросе для элементов.

Еще полезная вещь - атрибут выбора режима обработки ошибок <error-option>:

  • stop-on-error – прервать всю операцию при первой возникшей ошибке

  • continue-on-error — не останавливать процесс при появлении ошибки, но вернуть сообщение об ошибке.

  • Rollback-on-error – при возникновении ошибки откатить конфиг в состояния до начала операции. Эта функция является capability и должна поддерживаться устройством (urn:ietf:params:netconf:capability:rollback-on-error:1.0)

Ну что же, давайте уже вернемся в IDE да что-нибудь настроим.

from scrapli_netconf.driver import NetconfDriver

switch = {
    "host": "192.168.0.1",
    "auth_username": "scrapli",
    "auth_password": "scrapli",
    "auth_strict_key": False,
    "port": 22
}

CONFIG = """
<config>
  <ifm xmlns="urn:huawei:yang:huawei-ifm">
    <interfaces>
      <interface>
        <name>25GE1/0/1</name>
        <description>Test</description>
      </interface>
    </interfaces>
  </ifm>
</config>
"""
 
connection = NetconfDriver(**switch)
connection.open()

result = connection.edit_config(config=CONFIG, target='running')

connection.close()

Здесь мы, как нетрудно догадаться, установили описание порту 25GE1/0/1. Имя порта здесь, опять же, является ключом к списку, который не изменяется. Если этот элемент не указать, устройство на такой запрос вернет ошибку missing-element.

Давайте теперь удалим это описание. Изложим запрос в таком виде:

CONFIG = """
<config>
  <ifm xmlns="urn:huawei:yang:huawei-ifm" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
    <interfaces>
      <interface>
        <name>25GE1/0/1</name>
        <description nc:operation="remove"/>
      </interface>
    </interfaces>
  </ifm>
</config>
"""

Здесь нужно объяснить неразбериху с namespace. Давайте вспомним о том, что заданный в элементе namespace влияет на этот элемент и все вложенные, пока в каком-то вложенном элементе не определен другой namespace. По сути, это "переключатель контекста". Так, нам нужно выполнить операцию удаления для элемента description, но как говорилось выше, операции эти существуют в namespace urn:ietf:params:xml:ns:netconf:base:1.0, а мы в элементе <ifm> объявили xmlns="urn:huawei:yang:huawei-ifm". В нем не существует operation, а значит указание operation в элементе <description> заставит сервер вернуть нам ошибку. Надо как-то выкручиваться.

В элементе <ifm> мы опишем такую конструкцию: xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0". С помощью нее мы определяем для этого namespace префикс nc, чтобы далее обращаться к нему по этому префиксу, причем не для всего элемента, а для конкретного атрибута: nc:operation="remove"

Здесь мы применяем операцию удаления к одному конкретному элементу, поэтому удалится только он. Если же применить операцию к элементу, имеющему вложенные элементы, то удалятся они все. Таким образом можно разом удалять от одиночных элементов до целых блоков конфигурации. Например, если мы переместим nc:operation="remove" на уровень выше, то есть, в <interface>, удалится вся конфигурация интерфейса с заданным именем (само имя, понятно, не удалится).

Мы рассмотрели 2 действия: добавление конфигурации и ее удаление. Примеры являются простейшими, но, в сущности, ничего принципиально другого делать и не придется - просто усложнится структура добавляемой или удаляемой конфигурации.

Напоследок хочется упомянуть одну особенность, связанную с работой с модулем. Как видите, в качестве конфигурации он принимает XML, верхний уровень которого представлен контейнером <config>. Выше говорилось о таких вещах как <default-operation>, <error-option>. Нюанс в том, что эти элементы в запросе должны находится на одном уровне с <config>, то есть, наш запрос должен был бы выглядеть как-то так:

CONFIG = """
<default-operation>replace</default-operation>
<config>
  <ifm xmlns="urn:huawei:yang:huawei-ifm">
    <interfaces>
      <interface>
        <name>25GE1/0/1</name>
        <description>Test</description>
      </interface>
    </interfaces>
  </ifm>
</config>
"""

Однако метод модуля edit_config не принимает такие выкрутасы, он ожидает только контейнер <config> и ничего за его пределами видеть не хочет. Решается это применением другого метода - rpc. Этот метод позволяет использовать относительно "сырой" запрос, то есть содержимое контейнера <rpc>, а не контейнера <edit-config>. Значит, мы должны вручную указать операцию, например <edit-config>, а значит, и все сопутствующие опции мы также можем указать. Пример:

from scrapli_netconf.driver import NetconfDriver

switch = {
    "host": "192.168.0.1",
    "auth_username": "scrapli",
    "auth_password": "scrapli",
    "auth_strict_key": False,
    "port": 22
}

RPC = """
<edit-config>
  <target>
    <running/>
  </target>
  <default-operation>replace</default-operation>
  <config>
    <ifm xmlns="urn:huawei:yang:huawei-ifm">
      <interfaces>
        <interface>
          <name>25GE1/0/1</name>
          <description>Test</description>
        </interface>
      </interfaces>
    </ifm>
  </config>
</edit-config>
"""
 
connection = NetconfDriver(**switch)
connection.open()

result = connection.rpc(filter_=RPC)

connection.close()

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

Сохранение конфига

После изменения конфига нужно что? Сохранить его, конечно же. Netconf по умолчанию ничего не сохраняет, поэтому нужно это сделать отдельно. В модуле такого метода нет на момент написания этой статьи. Поэтому тут нам и пригодится метод rpc.

Netconf не оперирует понятием "сохранить конфиг", а оперирует операцией <copy-config>, то есть скопировать конфиг откуда-то куда-то. По сути, "сохранить конфиг" - означает скопировать running в startup, что netconf'у уже понятно. Запрос будет выглядеть так:

RPC = """
<copy-config>
  <target>
    <startup/>
  </target>
  <source>
    <running/>
  </source>
</copy-config>

Копировать можно и между другими хранилищами, что уже зависит от их наличия в ПО конкретного оборудования.

Полезный прием дебага

Иногда что-то идет не так и на на наш запрос сервер возвращает ошибку. В этом случае хочется увидеть весь запрос как есть, в том виде, в котором он отправляется на оборудование. Такая возможность есть, правда она не документирована в самом модуле. Поскольку он (запрос) сам по себе является элементом Etree библиотеки lxml (которую и использует модуль scrapli_netconf для сборки XML), нужно этот элемент превратить обратно в plain XML, чтобы его можно было вывести (28 строка кода ниже).

from scrapli_netconf.driver import NetconfDriver
from lxml import etree

switch = {
    "host": "192.168.0.1",
    "auth_username": "scrapli",
    "auth_password": "scrapli",
    "auth_strict_key": False,
    "port": 22
}

FILTER = """
<ifm xmlns="urn:huawei:yang:huawei-ifm">
  <interfaces>
    <interface>
      <name>25GE1/0/1</name>
      <description/>
    </interface>
  </interfaces>
</ifm>
"""
 
connection = NetconfDriver(**switch)
connection.open()

result = connection.get_config(filter_=FILTER, source='running')

request = etree.tostring(result.xml_input, pretty_print=True, encoding=str)
print(request)

connection.close()

Получим полный XML запроса:

<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
  <get-config>
    <source>
      <running/>
    </source>
    <filter type="subtree">
      <ifm xmlns="urn:huawei:yang:huawei-ifm">
        <interfaces>
          <interface>
            <name>25GE1/0/1</name>
            <description/>
          </interface>
        </interfaces>
      </ifm>
    </filter>
  </get-config>
</rpc>

Заключение

Пока я писал и редактировал эту статью, мне довелось вдоволь поупражняться и, к сожалению, я столкнулся со все теми же недостатками, которые описывал в начале, и про которые многие уже говорили до меня. А именно неполный охват конфигурации. Не все функции на оборудовании я смог настроить просто потому, что они не описаны в структуре данных. Тут, конечно, речь о Huawei; если у вас Juniper, то вы с таким наверняка не столкнетесь.

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