NETCONF Again¶
<get>
<get-config>
<edit-config>
<copy-config>
<delete-config>
<lock>
<unlock>
<close-session>
<kill-session>
Но зачастую вендоры определяют свои собственные операции.
Действия, операции¶
<get>¶
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get/> </rpc> ]]>]]>
<rpc-reply>
. В случае ошибки внутри <rpc-reply>
сервер вернёт <rpc-error>
с текстом ошибки.</get>
:<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <rpc-error> <error-type>protocol</error-type> <error-tag>operation-failed</error-tag> <error-severity>error</error-severity> <error-message>syntax error, expecting <filter> or </get></error-message> <error-info> <bad-element>interfaces</bad-element> </error-info> </rpc-error> </rpc-reply>
Или запросить несуществующую ветку:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> <interfaces/> </get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <rpc-error> <error-type>protocol</error-type> <error-tag>operation-failed</error-tag> <error-severity>error</error-severity> <error-message>syntax error, expecting <filter> or </get></error-message> <error-info> <bad-element>interfaces</bad-element> </error-info> </rpc-error> </rpc-reply>
<get>
будет содержаться либо вообще всё, что вам может дать устройство - полный конфиг и вся информацию по состоянию, либо какую-то часть.<get-interface-information>
:<rpc> <get-interface-information/> </rpc>
Вот такой будет ответ: https://pastebin.com/2xTpuSi3.
Этому, кстати, сложно найти объяснение. Довольно неудобно для каждой ветки операционных данных иметь собственный RPC. И более того, непонятно как это вообще описывается в моделях данных.
Очевидно, это не всегда (никогда) удобно. Хотелось бы пофильтровать данные. NETCONF позволяет не просто отфильтровать результат, а указать NETCONF-серверу, какую именно часть клиент желает запросить. Для этого используется элемент <filter>
.
<filter>¶
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get/> </rpc> ]]>]]>
Вот такой будет ответ: https://pastebin.com/MMWXM2eT.
С пустым фильтром не вернётся никаких данных.
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> <filter type="subtree"> </filter> </get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <database-status-information> <database-status> <user>eucariot</user> <terminal></terminal> <pid>31101</pid> <start-time junos:seconds="1644636396">2022-02-12 03:26:36 UTC</start-time> <edit-path></edit-path> </database-status> </database-status-information> </data> </rpc-reply> ]]>]]>
Вот таким запросом можно вытащить конфигурационные данные по всем интерфейсам
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> <filter type="subtree"> <configuration> <interfaces/> </configuration> </filter> </get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:changed-seconds="1644510087" junos:changed-localtime="2022-02-10 16:21:27 UTC"> <interfaces> <interface> <name>ge-0/0/0</name> <unit> <name>0</name> <family> <inet> <address> <name>169.254.0.1/31</name> </address> </inet> </family> </unit> </interface> <interface> <name>ge-0/0/2</name> <unit> <name>0</name> <family> <inet> <address> <name>169.254.100.1/31</name> </address> </inet> </family> </unit> </interface> <interface> <name>em0</name> <unit> <name>0</name> <family> <inet> <address> <name>192.168.1.2/24</name> </address> </inet> </family> </unit> </interface> </interfaces> </configuration> <database-status-information> <database-status> <user>eucariot</user> <terminal></terminal> <pid>31101</pid> <start-time junos:seconds="1644636721">2022-02-12 03:32:01 UTC</start-time> <edit-path></edit-path> </database-status> </database-status-information> </data> </rpc-reply> ]]>]]>
Если вы хотите выбрать не все элементы дерева, а только интересующую вас часть, то можно указать, какие именно нужны:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> <filter type="subtree"> <configuration> <interfaces> <interface> <name/> <description/> </interface> </interfaces> </configuration> </filter> </get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:changed-seconds="1644637011" junos:changed-localtime="2022-02-12 03:36:51 UTC"> <interfaces> <interface> <name>ge-0/0/0</name> <description>kzn-leaf-0</description> </interface> <interface> <name>ge-0/0/2</name> <description>kzn-edge-0</description> </interface> <interface> <name>em0</name> <description>mgmt-switch</description> </interface> </interfaces> </configuration> <database-status-information> <database-status> <user>eucariot</user> <terminal></terminal> <pid>31316</pid> <start-time junos:seconds="1644637103">2022-02-12 03:38:23 UTC</start-time> <edit-path></edit-path> </database-status> </database-status-information> </data> </rpc-reply> ]]>]]>
При этом если хочется забрать данные только по конкретному интерфейсу:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> <filter type="subtree"> <configuration> <interfaces> <interface> <name>ge-0/0/0</name> </interface> </interfaces> </configuration> </filter> </get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:changed-seconds="1644637011" junos:changed-localtime="2022-02-12 03:36:51 UTC"> <interfaces> <interface> <name>ge-0/0/0</name> <description>kzn-leaf-0</description> <unit> <name>0</name> <family> <inet> <address> <name>169.254.0.1/31</name> </address> </inet> </family> </unit> </interface> </interfaces> </configuration> <database-status-information> <database-status> <user>eucariot</user> <terminal></terminal> <pid>31316</pid> <start-time junos:seconds="1644637321">2022-02-12 03:42:01 UTC</start-time> <edit-path></edit-path> </database-status> </database-status-information> </data> </rpc-reply> ]]>]]>
Соответственно можно совместить запрос конкретного интерфейса и только тех его полей, которые интересны.
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get> <filter type="subtree"> <configuration> <interfaces> <interface> <name>ge-0/0/0</name> <description/> </interface> </interfaces> </configuration> </filter> </get> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:changed-seconds="1644637011" junos:changed-localtime="2022-02-12 03:36:51 UTC"> <interfaces> <interface> <name>ge-0/0/0</name> <description>kzn-leaf-0</description> </interface> </interfaces> </configuration> <database-status-information> <database-status> <user>eucariot</user> <terminal></terminal> <pid>31316</pid> <start-time junos:seconds="1644637396">2022-02-12 03:43:16 UTC</start-time> <edit-path></edit-path> </database-status> </database-status-information> </data> </rpc-reply> ]]>]]>
Ещё немного про subtree filtering.
<get>
ничем практически не отличается от <get-config>
. Для того, чтобы забрать операционные данные, нужно воспользоваться другими операциями - специфическими под каждую задачу.show version | display xml rpc
<get>
удобно забирать операционные данные с устройства. Например, для мониторинга. Или для отладки. Можно выбрать всех BGP-соседей в состоянии Idle, или все интерфейсы с ошибками, данные по маршрутам.<get-config>¶
<get-config>
- это поддерево <get>
, но это всё-таки не так.С помощью <get-config>
можно указать из какого источника мы хотим получить конфигу - running
, candidate
, startup
итд.
Забираем текущий конфиг:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get-config> <source> <running/> </source> </get-config> </rpc> ]]>]]>
<get-config>
так же, как и <get>
позволяет использовать элемент <filter>
. Например:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get-config> <source> <running/> </source> <filter type="subtree"> <configuration> <system> <host-name/> </system> </configuration> </filter> </get-config> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:commit-seconds="1644637011" junos:commit-localtime="2022-02-12 03:36:51 UTC" junos:commit-user="eucariot"> <system> <host-name>kzn-spine-0</host-name> </system> </configuration> </data> </rpc-reply> ]]>]]>
В запросе самые внимательные обратили внимание на элемент <source>
.
Configuration Datastores¶
<running>
- это текущая актуальная конфигурация.<candidate>
, <startup>
и какие-то другие.Соответственно запросить конфигурацию можно из разных Datastores при их наличии, указывая соответствующий элемент внутри <source>
.
<target>
.<running>
, а кто-то только <candidate>
с последующим <commit>
.<edit-config>¶
ЕЙ богу, самая интересная штука во всём NETCONF! Операция, с помощью которой можно привести конфигурацию к нужному состоянию. Серебряная пуля, панацея, окончательное решение конфигурационного вопроса. Ага, щаз! Идея в теории прекрасна: мы отправляем на устройство желаемую конфигурацию в виде XML, а оно само шуршит и считает, что нужно применить, а что удалить. Давайте идеальный случай и разберём сначала.
<edit-config>
позволяет загрузить полную конфигурацию устройства или его часть в указанный datastore. При этом устройство сравнивает актуальную конфигурацию в datastore и передаваемую с клиента и предпринимает указанные действия.operation
в любом из элементов поддерева <configuration>
. Operation может встречаться несколько раз в XML и быть при этом разным. Атрибут может принимать следующие значения:- Merge - новая конфига вливается в старую - что необходимо заменить - заменяется, новое - добавляется, ничего не удаляется.
- Replace - заменяет старую конфигурацию новой.
- Create - создаёт блок конфигурации. Однако, если он уже существует, вернётся
<rpc-error>
- Delete - удаляет блок конфигурации. Однако, если его не существует, вернётся
<rpc-error>
- Remove - удаляет блок конфигурации. Однако, если его не существует, проигнорирует. Определён в RFC6241.
Если тип операции не задан, то новая конфигурация будет вмёржена в старую. Задать операцию по умолчанию можно с помощью параметра <default-operation>
: merge
, replace
, none
.
В дереве <configuration>
задаётся собственно целевая конфигурация в виде XML.
Безусловно, самая интересная операция внутри <edit-config>
- это replace. Ведь она предполагает, что устройство возьмёт конфигурацию из RPC и заменит ею ту, что находится в datastore. А где-то там под капотом и крышкой блока цилиндров система сама просчитает дельту, которую нужно отправить на чипы.
Практика edit-config¶
Давайте сначала что-то простое: поменяет hostname:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <edit-config> <target> <candidate/> </target> <config> <configuration> <system> <host-name>just-for-lulz</host-name> </system> </configuration> </config> </edit-config> </rpc> ]]>]]>
Проверяем, что в кандидат-конфиге эти изменения есть, а в текущем - нет
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get-config> <source> <candidate/> </source> <filter type="subtree"> <configuration> <system> <host-name/> </system> </configuration> </filter> </get-config> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:changed-seconds="1644719855" junos:changed-localtime="2022-02-13 02:37:35 UTC"> <system> <host-name>just-for-lulz</host-name> </system> </configuration> </data> </rpc-reply> ]]>]]>
Проверяем running:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get-config> <source> <running/> </source> <filter type="subtree"> <configuration> <system> <host-name/> </system> </configuration> </filter> </get-config> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:commit-seconds="1644637011" junos:commit-localtime="2022-02-12 03:36:51 UTC" junos:commit-user="eucariot"> <system> <host-name>kzn-spine-0</host-name> </system> </configuration> </data> </rpc-reply>
Значит, надо закоммитить изменения.
<rpc> <commit/> </rpc> ]]>]]> <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos"> <ok/> </rpc-reply>
Проверяем running:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get-config> <source> <running/> </source> <filter type="subtree"> <configuration> <system> <host-name/> </system> </configuration> </filter> </get-config> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:commit-seconds="1644720065" junos:commit-localtime="2022-02-13 02:41:05 UTC" junos:commit-user="eucariot"> <system> <host-name>just-for-lulz</host-name> </system> </configuration> </data> </rpc-reply>
На Juniper доступны в NETCONF те же функции коммитов, что и в CLI. Например, commit confirmed
и confirmed-timeout
.
А теперь что-то посложнее и с операцией replace
: заменим список BGP-пиров:
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <edit-config> <target> <candidate/> </target> <config> <configuration> <protocols> <bgp operation="replace"> <group> <name>LEAFS</name> <type>external</type> <import>ALLOW</import> <family> <inet> <unicast> </unicast> </inet> </family> <export>EXPORT</export> <neighbor> <name>169.254.0.0</name> <peer-as>64513.00000</peer-as> </neighbor> </group> <group> <name>EDGES</name> <type>external</type> <import>ALLOW</import> <family> <inet> <unicast> </unicast> </inet> </family> <export>EXPORT</export> <neighbor> <name>222.222.222.0</name> <peer-as>65535</peer-as> </neighbor> </group> </bgp> </protocols> </configuration> </config> </edit-config> </rpc> ]]>]]>
Коммит
<rpc> <commit/> </rpc> ]]>]]>
Проверяем running
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <get-config> <source> <running/> </source> <filter type="subtree"> <configuration> <protocols> <bgp> <group> <neighbor/> </group> </bgp> </protocols> </configuration> </filter> </get-config> </rpc> ]]>]]> <rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params: xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:commit-seconds="1644720678" junos:commit-localtime="2022-02-13 02:51:18 UTC" junos:commit-user="eucariot"> <protocols> <bgp> <group> <name>LEAFS</name> <neighbor> <name>169.254.0.0</name> <peer-as>64513.00000</peer-as> </neighbor> </group> <group> <name>EDGES</name> <neighbor> <name>222.222.222.0</name> <peer-as>65535</peer-as> </neighbor> </group> </bgp> </protocols> </configuration> </data> </rpc-reply>
Всё сработало)
А теперь попробуем операцию merge при добавлении нового пира.
<rpc message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <edit-config> <target> <candidate/> </target> <config> <configuration> <protocols> <bgp operation="merge"> <group> <name>LEAFS</name> <type>external</type> <import>ALLOW</import> <family> <inet> <unicast> </unicast> </inet> </family> <export>EXPORT</export> <neighbor> <name>169.254.0.0</name> <peer-as>64513.00000</peer-as> </neighbor> </group> <group> <name>EDGES</name> <type>external</type> <import>ALLOW</import> <family> <inet> <unicast> </unicast> </inet> </family> <export>EXPORT</export> <neighbor> <name>222.222.222.0</name> <peer-as>65535</peer-as> </neighbor> <neighbor> <name>169.254.100.0</name> <peer-as>65535</peer-as> </neighbor> </group> </bgp> </protocols> </configuration> </config> </edit-config> </rpc> ]]>]]>
Коммит
<rpc> <commit/> </rpc> ]]>]]>
Проверка:
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/14.1R1/junos" message-id="100" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> <data> <configuration xmlns="http://xml.juniper.net/xnm/1.1/xnm" junos:commit-seconds="1644721481" junos:commit-localtime="2022-02-13 03:04:41 UTC" junos:commit-user="eucariot"> <protocols> <bgp> <group> <name>LEAFS</name> <neighbor> <name>169.254.0.0</name> <peer-as>64513.00000</peer-as> </neighbor> </group> <group> <name>EDGES</name> <neighbor> <name>222.222.222.0</name> <peer-as>65535</peer-as> </neighbor> <neighbor> <name>169.254.100.0</name> <peer-as>65535</peer-as> </neighbor> </group> </bgp> </protocols> </configuration> </data> </rpc-reply> ]]>]]>
replace
и merge
.Operation replace¶
replace
следует иметь в виду некоторые нюансы. Например, что нужно передавать полную конфигурацию того или иного сервиса или функциональности - не просто новые параметры - ведь железка натурально заменит то, что было, тем, что прилетело. Едва ли вы хотите создав один интерфейс в OSPF Area, удалить остальные?Использовать replace можно как на уровне отдельных частей конфигурации, так и на верхнем уровне, требуя заменить всё поддерево.
Однако ещё один нюанс заключается в том, что в зависимости от реализации вычисление дельты может занять много ресурсов CPU. Поэтому, если собираетесь кинуть диф на 13 000 строк политик BGP, то дважды подумайте и трижды оттестируйте, что после этого происходит с коробкой.
<commit>¶
<commit>
не замещает running на candidate, как это делает <copy-config>
, а выполняет именно применение конфигурационной дельты, как это происходит в CLI.Как и в CLI у commit
может быть параметр confirmed
, заставляющий откатить изменения, если commit не был подтверждён. За это отвечает отдельная capability: confirmed-commit
.
<commit>
не входит в число базовых операций, поскольку как раз зависит от поддерживаемых возможностей сервера.
<copy-config>¶
Операция заменяет одну конфигурацию другой. Имеет два параметра: source
- откуда - и target
- куда.
Может использоваться как для применения новой конфигурации на коробку, так и для бэкапа активной.
Если коробка поддерживает capability :url
, то в качестве source
и/или target
может быть указан URL.
<delete-config>¶
Очевидно, удаляет конфигурацию из target datastore. Без хитростей.
<lock/unlock>¶
Аналогично Juniper CLI ставит блок на target datastore от совместного редактирования, чтобы не было конфликта. Причём блок должен работать как на NETCONF, так и на другие способы изменения конфигурации - SNMP, CLI, gRPC итд.
<close-session>¶
Аккуратно закрывает существующую NETCONF-сессию, снимает локи, высвобождает ресурсы.
<kill-session>¶
Грубо разрывает сессиию, но снимает локи. Если сервер получил такую операцию в тот момент, когда он дожидается confirmed commit, он должен отменить его и откатить изменения к состоянию, как было до установки сессии.
Инструменты разработчика для NETCONF¶
И я думаю, к этому моменту вам уже очевидно, что отправка XML через SSH с ручным проставлением Framing Marker (]]>]]>
) - не самый удобный способ. Давайте посмотрим на существующие библиотеки.
netconf-console¶
Прежде чем писать какой-то код, обычно стоит проверить всё руками. Но вот руками крафтить XML и проставлять framing marker’ы тоскливо. Тут отца русской автоматизации спасёт netconf-console
- главный и, возможно, единственный CLI-инструмент для работы с NETCONF.
Может работать в режиме команды:
netconf-console --host 192.168.1.2 --port 22 -u eucariot -p password --get-config
А может в интерактивном:
netconf-console2 --host 192.168.1.2 --port 22 -u eucariot -p password -i netconf> hello
NCclient¶
from ncclient import manager if __name__ == "__main__": with manager.connect( host="kzn-spine-0.juniper", ssh_config=True, hostkey_verify=False, device_params={'name': 'junos'} ) as m: c = m.get_config(source='running').data_xml print(c)Дабы уберечь читателя от многочасовых мук с отладкой аунтентификации, небольшая подсказка тут. Текущая версия
paramiko
на момент написания статьи (>=2.9.0), которую подтягиваетncclient
, в ряде случае не может работать с OpenSSH-ключами и падает с ошибкой «Authentication failed». Рекомендую в этом случае устанавливать 2.8.0. На гитхабе открыта куча issue на эту тему. И, кажется, его даже починили, но я не проверял. И вроде бы даже есть решение, но и это я не проверял.
Так же работают filter:
from ncclient import manager rpc = """ <filter> <configuration> <system> <host-name/> </system> </configuration> </filter> """ if __name__ == "__main__": with manager.connect( host="kzn-spine-0.juniper", ssh_config=True, hostkey_verify=False, device_params={"name": "junos"} ) as m: c = m.get_config("running", rpc).data_xml print(c)
С таким вот результатом:
<?xml version="1.0" encoding="UTF-8"?> <rpc-reply message-id="urn:uuid:864dd143-7a86-40ca-8992-5a35f2322ea0"> <data> <configuration commit-seconds="1644732354" commit-localtime="2022-02-13 06:05:54 UTC" commit-user="eucariot"> <system> <host-name> kzn-spine-0 </host-name> </system> </configuration> </data> </rpc-reply>
На текстовый XML смотреть не надо - парсим библиотечкой xmltodict:
from ncclient import manager import xmltodict rpc = """ <filter> <configuration> <system> <host-name/> </system> </configuration> </filter> """ if __name__ == "__main__": with manager.connect( host="kzn-spine-0.juniper", ssh_config=True, hostkey_verify=False, device_params={"name": "junos"} ) as m: result = m.get_config("running", rpc).data_xml result_dict = xmltodict.parse(result) print(f'hostname is {result_dict["rpc-reply"]["data"]["configuration"]["system"]["host-name"]}')
С уже таким результатом:
hostname is kzn-spine-0При работе с сетевыми коробками по NETCONF xmltodict, пожалуй, самая практичная библиотека, преобразующая XML-данные в объект Python. Она использует C-шный парсер pyexpat, так что недостатков у такого подхода фактически нет.
Точно так же можно обновить конфигурацию в два действия: <edit-config>
в <candidate>
и <commit>
:
from ncclient import manager import xmltodict rpc = """ <config> <configuration> <interfaces> <interface> <name>ge-0/0/0</name> <description>Mit der Dummheit kämpfen Götter selbst vergebens.</description> </interface> </interfaces> </configuration> </config> """ if __name__ == "__main__": with manager.connect( host="kzn-spine-0.juniper", ssh_config=True, hostkey_verify=False, device_params={"name": "junos"} ) as m: result = m.edit_config(target="candidate", config=rpc).data_xml m.commit() result_dict = xmltodict.parse(result) print(result_dict) OrderedDict([('rpc-reply', OrderedDict([('@message-id', 'urn:uuid:93bde991-81f9-42d6-a343-b4fc267646c2'), ('ok', None)]))])
Дальше пока копать не будем. Тем более, бытует мнение «без всяких сомнений, самый ублюдочно написанный Python код, что я видел в opensource»
scrapli-netconf¶
Давайте взглянем на пару примеров работы.
from scrapli_netconf.driver import NetconfDriver rpc = """ <filter> <configuration> <system> <host-name/> </system> </configuration> </filter> """ device = { "host": "kzn-spine-0.juniper", "auth_strict_key": False, "port": 22 } if __name__ == "__main__": with NetconfDriver(**device) as conn: response = conn.get_config("running", rpc) print(response.result)
Как это использовать¶
Мониторинг¶
NETCONF предоставляет возможность собирать операционные данные:
- Состояния протоколов (OPSF, BGP-пиринги)
- Статистику интерфейсов
- Утилизацию ресурсов CPU
- Таблицы маршрутизации
- Другое
- Используем безопасный SSH, не используем SNMP
- Не несём дополнительные протоколы в сеть
- Полная свобода того, какие данные мы собираем, без необходимости разбираться в OID’ах и MIB’ах
- При этом есть возможность собирать данные в соответствии с YANG-моделью
- Гипотетическая возможность оформить подписку на события в системе
Выполнение отдельных операций¶
Configuration Management¶
Да, это тоже возможно, если
Оборудование поддерживает 100% конфигурации через NETCONF. Увы, я на своём веку повидал ситуаций, когда некоторые секции просто-напросто отсутствовали в NETCONF и никакого способа настроить нужную функцию нет.
- Оборудование честно поддерживает операцию «replace», без этого вычисление конфигурационной дельты ложится вновь на сетевиков.Однако, в том виде, в котором мы познакомились с темой на данный момент, дальше начинается Jinja-программирование. Каждому, кто этим занимался, обычно неловко, и он стыдливо избегает разговора на эту тему.Задача решается примерно следующим образом:
- Пишем циклопические развесистые jinja-шаблоны с ифами и форами, внутри которых XML. Шаблоны под каждого вендора, конечно, свои собственные, поскольку и схемы данных у них разные. Но при этом они универсальные в плане ролей устройств - не нужно для свитчей доступа и маршрутизаторов ядра писать разные шаблоны - просто в зависимости от роли будут активироваться те или иные их части.Здесь в нужных местах сразу описаны типы операций - где merge, где replace.
Каким-то образом формируем под каждое устройство файлы переменных, в которых указаны хостнеймы, IP-адреса, ASN, пиры и прочие специфические вещи. Эти файлы переменных в свою очередь, напротив, вендор-нейтральны, но будут отличаться от роли к роли.
Рендерим конфигурацию в формате XML, накладывая переменные на шаблоны. Получаем целевую конфигурацию в виде дерева XML, где в нужных местах проставлена операция
replace
.Этот XML с помощью ncclient, ansible, scrapli-netconf или чего-то ещё подпихиваем на коробку.
NETCONF-сервер на коробке получает RPC и вычисляет конфигурационный патч, который фактически применит. То есть он находит разницу между целевой конфигурацией в RPC и текущей в
<running>
. Применяет эту конфигурацию.
Как бы это могло выглядеть я уже показывал в предыдущем выпуске АДСМ.
Ручная правка файлов переменных - это очень неудобно, конечно же. Просто мрак, если мы говорим про какие-то типовые вещи, как например датацентровые регулярные топологии. Новая пачка стоек - сотни и тысячи строк для копипащения и ручного изменения. Но на самом деле их можно создавать автоматически на основе данных из централизованной базы данных - DCIM/IPAM.
Что тут хорошо:
- Изменения в Jinja-шаблонах версионируются через git и проходят проверку другими инженерами перед применением. Это систематические изменения, влияющие на большое количество устройств.
- Изменения в переменных - точно так же. Это точечное изменение конкретного устройства.
- Только после согласования изменений в пунктах выше, можно сгенерировать новую конфигурацию и далее уже её отправить на проверку в git.
- Если соблюдать процесс, то отсутствует конфигурационный дрейф.
Что тут плохо?
- Ну, очевидно, Jinja-программирование
- Работа с текстом, вместо объектов языка.
- Отсутствие возможности взглянуть на конфигурационный диф до его применения.
На этом на самом деле заканчивается первая большая часть этой статьи, которая позволяет просто уже взять и получать пользу от NETCONF в задачах автоматизации.
Я вот прям серьёзно сейчас, ей богу! Не туманные абстракции - берём NETCONF - и на многих вендорах уже можно с ним работать выстраивая автоматизацию того или иного объёма.
Хух. Давайте просто не будем об этом сейчас? Просто не сейчас? Попозже. После RESTCONF и gRPC?