YANG для чайников
Источник: https://napalm-automation.net/yang-for-dummies/
Автор: Дэвид Барросо
Дата публикации оригинальной статьи: 14 августа 2017 года
Изначально я написал этот пост как часть документации к napalm-yang, но потом понял, что он мог быть интересен более широкому кругу людей, а не только для тех, кто решит использовать данный проект.
В этом посте мы попытаемся объяснить общие идеи YANG, почему он интересен, и как его можно использовать для описания данных с одной стороны очень замысловатым, но с другой гибким и мощным способом.
Обратите внимание, что это всего лишь введение. Если вы хотите полностью понять, как работает YANG, я рекомендую прочитать RFC6020. Для napalm-yang я советую обратиться к этому руководству.
Что такое YANG¶
YANG это язык моделирования данных. Он используется для описания того, как должны выглядеть данные. YANG это не XML или JSON, YANG это язык, описывающий модели данных. При этом данные могут быть в виде JSON или XML структуры.
Терминология¶
Чтобы мы с вами говорили на одном языке, для начала необходимо разобраться в терминологии. Выдержка из RFC6020:
- augment: Добавляет новые узлы схемы в другой ранее определенный узел.
- container: Внутренний узел данных, который существует в единственном экземпляре в дереве данных. Контейнер сам по себе не имеет значения, а содержит дочерние узлы.
- data model: Модель данных, описывающая как данные должны быть представлены и каким образом к ним можно получить доступ.
- data node: Узел в дереве схемы, который может быть создан в дереве данных: container, leaf, leaf-list и anyxml.
- data tree: Созданное дерево данных конфигурации и состояния на устройстве.
- derived type: Тип, производный от встроенного типа (например такого как uint32) или другого производного типа.
- device deviation: Отказ устройства в возможности верно реализовать модуль.
- grouping: Повторно используемый набор узлов схемы, который может локально использоваться в модуле, в других модулях, которые его включают или импортируют из него. Оператор группировки не является оператором определения данных и, как таковой, не определяет никаких узлов в дереве схемы.
- identifier: Используется для идентификации разных элементов YANG по имени.
- leaf: Узел в дереве данных, который существует в единственном экземпляре, имеет значение и не имеет потомков.
- leaf-list: То же самое, что и leaf, но содержит несколько узлов и их значений, которые в свою очередь не имеют потомков.
- list: Внутренний узел данных, который может иметь несколько экземпляров в дереве данных. Не имеет значения, но может иметь множество дочерних узлов.
- module: YANG модуль определяет иерархию узлов, которые могут использоваться в операциях NETCONF. Модуль является автономным и компилируемым с его определениями и определениями, которые он импортирует или включает откуда-либо.
- state data: Дополнительные данные, не относящиеся к конфигурации, доступные только в режиме чтения, а также накопленная статистика - RFC4741.
Вселенная Звездные Войны¶
Давайте попробуем понять YANG на примере. Представьте на секунду, что вы хотите описать полную структуру Звездных Войн. Единую структуру, управляющую всеми ее частями (неправильное кино).
Давайте начнем с возможности добавлять персонажей во вселенную Звездных Войн. Эти персонажи будут иметь следующие характериситики:
- name - у всех есть имя, даже если это номер модели. Здесь мы ничего не выдумаваем. Имя - просто строка.
- age - возраст ограничим числом 2000, потому как кто хочет жить вечно (неправильное кино). Здесь мы должны создать тип, который будем использовать для проверки корректности данных:
typedef age {
type uint16 {
range 1..2000;
}
}
- affilation - принадлежность. Либо вы с империей, либо против нее. Создадим идентификаторы, которые однозначно определяют возможные принадлежности:
identity AFFILIATION {
description "К какой группе принадлежит кто-либо";
}
identity EMPIRE {
base AFFILIATION;
description "Принадлежит империи";
}
identity REBEL_ALLIANCE {
base AFFILIATION;
description "Принадлежит альянсу";
}
Этого достаточно, чтобы создать модель данных:
// имя модуля
module napalm-star-wars {
// шаблон
yang-version "1";
namespace "https://napalm-yang.readthedocs.io/napalm-star-wars";
prefix "napalm-star-wars";
// однозначный идентификатор фракции, к которой принадлежит персонаж
identity AFFILIATION {
description "К какой группе принадлежит кто-либо";
}
identity EMPIRE {
base AFFILIATION;
description "Принадлежит империи";
}
identity REBEL_ALLIANCE {
base AFFILIATION;
description "Принадлежит альянсу";
}
// тип, определяющий корректность данных - возраст
typedef age {
type uint16 {
range 1..2000;
}
}
// группируем данные, которые будут назначены персонажам
grouping personal-data {
leaf name {
type string;
}
leaf age {
type age;
}
leaf affiliation {
type identityref {
base napalm-star-wars:AFFILIATION;
}
}
}
// корневой объект, определенный моделью
container universe {
list individual {
// идентифицируем каждого персонажа по его имени
key "name";
// каждый персонаж должен иметь параметры определенные в группе
uses personal-data;
}
}
}
Сначала мы создаем набор метаданных, определяющих принадлежность к фракции и возраст персонажа. Далее объединяем эти метаданные в группу персональных данных, которые относятся к каждому персонажу. И в конце определяем контейнер, в котором создаем список персонажей. При этом ключевым параметром персонажа является его имя.
Использование модели¶
Теперь давайте представим модель в виде дерева
$ pyang -f tree napalm-star-wars.yang
module: napalm-star-wars
+--rw roster
+--rw individual* [name]
+--rw name string
+--rw age? age
+--rw affiliation? identityref
Именно то, что мы ожидали. Давайте теперь сделаем с этим что-нибудь полезное, а именно создаим python
код на основе данной модели. Для этого можно использовать pyangbind
(под капотом используется napalm-yang
):
Warning
В данный момент автор статьи прекратил поддерживать библиотеку napalm-yang
и предлагает вместо нее использовать yangify
$ export PYBINDPLUGIN=`/usr/bin/env python -c \
'import pyangbind; import os; print "%s/plugin" % os.path.dirname(pyangbind.__file__)'`
$ pyang --plugindir $PYBINDPLUGIN -f pybind napalm-star-wars.yang > napalm_star_wars.py
Теперь у нас есть некий python
код, который мы можем использовать:
>>> import napalm_star_wars
>>>
>>> sw = napalm_star_wars.napalm_star_wars()
>>>
>>> obi = sw.universe.individual.add("Obi-Wan Kenobi")
>>> obi.affiliation = "REBEL_ALLIANCE"
>>> obi.age = 57
>>>
>>> luke = sw.universe.individual.add("Luke Skywalker")
>>> luke.affiliation = "REBEL_ALLIANCE"
>>> luke.age = 19
>>>
>>> darth = sw.universe.individual.add("Darth Vader")
>>> darth.affiliation = "EMPIRE"
>>> darth.age = 42
>>>
>>> yoda = sw.universe.individual.add("Yoda")
>>> yoda.affiliation = "REBEL_ALLIANCE"
>>> yoda.age = 896
>>>
>>> import json
>>> print(json.dumps(sw.get(), indent=4))
{
"universe": {
"individual": {
"Obi-Wan Kenobi": {
"affiliation": "REBEL_ALLIANCE",
"age": 57,
"name": "Obi-Wan Kenobi"
},
"Luke Skywalker": {
"affiliation": "REBEL_ALLIANCE",
"age": 19,
"name": "Luke Skywalker"
},
"Darth Vader": {
"affiliation": "EMPIRE",
"age": 42,
"name": "Darth Vader"
},
"Yoda": {
"affiliation": "REBEL_ALLIANCE",
"age": 896,
"name": "Yoda"
}
}
}
}
Круто! Давайте теперь создадим Боба Фетта:
>>> boba = sw.universe.individual.add("Boba Fett")
>>> boba.affiliation = "MERCENARY"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "napalm_star_wars.py", line 165, in _set_affiliation
'generated-type': """YANGDynClass(base=RestrictedClassType(base_type=unicode, restriction_type="dict_key", restriction_arg={u'napalm-star-wars:EMPIRE': {'@namespace': u'https://napalm-yang.readthedocs.io', '@module': u'napalm-star-wars'}, u'EMPIRE': {'@namespace': u'https://napalm-yang.readthedocs.io', '@module': u'napalm-star-wars'}, u'napalm-star-wars:REBEL_ALLIANCE': {'@namespace': u'https://napalm-yang.readthedocs.io', '@module': u'napalm-star-wars'}, u'REBEL_ALLIANCE': {'@namespace': u'https://napalm-yang.readthedocs.io', '@module': u'napalm-star-wars'}},), is_leaf=True, yang_name="affiliation", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, namespace='https://napalm-yang.readthedocs.io', defining_module='napalm-star-wars', yang_type='identityref', is_config=True)""",
ValueError: {'error-string': 'affiliation must be of a type compatible with identityref', 'generated-type': 'YANGDynClass(base=RestrictedClassType(base_type=unicode, restriction_type="dict_key", restriction_arg={u\'napalm-star-wars:EMPIRE\': {\'@namespace\': u\'https://napalm-yang.readthedocs.io\', \'@module\': u\'napalm-star-wars\'}, u\'EMPIRE\': {\'@namespace\': u\'https://napalm-yang.readthedocs.io\', \'@module\': u\'napalm-star-wars\'}, u\'napalm-star-wars:REBEL_ALLIANCE\': {\'@namespace\': u\'https://napalm-yang.readthedocs.io\', \'@module\': u\'napalm-star-wars\'}, u\'REBEL_ALLIANCE\': {\'@namespace\': u\'https://napalm-yang.readthedocs.io\', \'@module\': u\'napalm-star-wars\'}},), is_leaf=True, yang_name="affiliation", parent=self, path_helper=self._path_helper, extmethods=self._extmethods, register_paths=True, namespace=\'https://napalm-yang.readthedocs.io\', defining_module=\'napalm-star-wars\', yang_type=\'identityref\', is_config=True)', 'defined-type': 'napalm-star-wars:identityref'}
Оказывается, что наша модель поддерживает только повстанцев и членов империи. Все верно, не правда ли?
Расширенная вселенная Звездных Войн¶
Наш фреймвор оказался на столько успешным, что люди начали добавлять в него свои модификации. Одна из таких модификаций добавляет поддержку фракции наемников для персонажей, а так же дополнительные данные, которые указывают на то находится персонаж на службе или в отставке. YANG очень удобный в плане расширения существующей модели. При этом нет необходимости создавать отдельную ветку проекта, менять схему или делать что-либо еще. Достаточно импортировать старую модель и добавить новые элементы. Давайте посмотрим, как будет выглядеть расширения для нашей существующей модели:
module napalm-star-wars-extended {
yang-version "1";
namespace "https://napalm-yang.readthedocs.io/napalm-star-wars-extended";
prefix "napalm-star-wars-extended";
// Импортируем старую модель
import napalm-star-wars { prefix napalm-star-wars; }
// Новая фракция, к которой может относиться персонаж,
// основанная на AFFILIATION из старой модели
identity MERCENARY {
base napalm-star-wars:AFFILIATION;
description "Друг за деньги";
}
// Данная группировка содержит новую информацию, которую мы хотим
// добавить к персональным данным в старой модели
grouping extended-personal-data {
leaf status {
type enumeration {
enum ACTIVE {
description "На службе";
}
enum RETIRED {
description "Наслаждается заслуженным отдыхом где-нибудь в доме у озера";
}
}
}
}
// Здесь мы указываем, какую часть старой мдели мы хотим расширить
augment "/napalm-star-wars:universe/napalm-star-wars:individual" {
uses extended-personal-data;
}
}
Легко, не правда ли? Изящность состоит в том, что вы может загрузить расширение если захотите, а также, если кто-то изменит оригинальную модель, вы автоматически получите приемущества этих изменений, так как просто наследуете эту модель. Давайте еще раз проделаем то, что делали раньшне, чтобы увидеть пользу расширений. Представление в виде дерева выглядит отлично:
$ pyang -f tree napalm-star-wars-extended.yang napalm-star-wars.yang
module: napalm-star-wars
+--rw universe
+--rw individual* [name]
+--rw name string
+--rw age? age
+--rw affiliation? identityref
+--rw napalm-star-wars-extended:status? enumeration
Теперь создадим python
код с установленным расширением:
$ pyang --plugindir $PYBINDPLUGIN -f pybind napalm-star-wars-extended.yang napalm-star-wars.yang > napalm_star_wars_extended.py
И воспользуемся им:
>>> import napalm_star_wars_extended
>>>
>>> sw = napalm_star_wars_extended.napalm_star_wars()
>>>
>>> obi = sw.universe.individual.add("Obi-Wan Kenobi")
>>> obi.affiliation = "REBEL_ALLIANCE"
>>> obi.age = 57
>>> obi.status = "RETIRED"
>>>
>>> darth = sw.universe.individual.add("Darth Vader")
>>> darth.affiliation = "EMPIRE"
>>> darth.age = 42
>>> darth.status = "ACTIVE"
>>>
>>> yoda = sw.universe.individual.add("Yoda")
>>> yoda.affiliation = "REBEL_ALLIANCE"
>>> yoda.age = 896
>>> yoda.status = "RETIRED"
>>>
>>> boba = sw.universe.individual.add("Boba Fett")
>>> boba.affiliation = "MERCENARY"
>>> boba.age = 32
>>> boba.status = "ACTIVE"
>>>
>>> import json
>>> print(json.dumps(sw.get(), indent=4))
{
"universe": {
"individual": {
"Obi-Wan Kenobi": {
"status": "RETIRED",
"affiliation": "REBEL_ALLIANCE",
"age": 57,
"name": "Obi-Wan Kenobi"
},
"Darth Vader": {
"status": "ACTIVE",
"affiliation": "EMPIRE",
"age": 42,
"name": "Darth Vader"
},
"Yoda": {
"status": "RETIRED",
"affiliation": "REBEL_ALLIANCE",
"age": 896,
"name": "Yoda"
},
"Boba Fett": {
"status": "ACTIVE",
"affiliation": "MERCENARY",
"age": 32,
"name": "Boba Fett"
}
}
}
}
Отлично, в новой расширенной версии нашей модели теперь мы знаем, наслаждается персонаж безбедной пенсией или нет, а также добавляем возможность использовать дополнительную фракцию, к которой он может принадлежать.
Реальный пример¶
Давайте теперь посмотрим, как napalm-yang
использует расширяемость YANG. Если вы взглянете на модель openconfig-if-ip, то увидите, что она не поддерживает опцию secondary
, которая обязательно для некоторых платформ, когда вы пытаетесь настроить несколько IPv4 адресов на одном интерфейсе. Не велика беда. Можем это исправить самостоятельно:
module napalm-if-ip {
yang-version "1";
namespace "https://github.com/napalm-automation/napalm-yang/yang_napalm/interfaces";
prefix "napalm-ip";
import openconfig-interfaces { prefix oc-if; }
import openconfig-vlan { prefix oc-vlan; }
import openconfig-if-ip { prefix oc-ip; }
organization "NAPALM Automation";
contact "napalm-automation@googlegroups.com";
description "Данные модуль определяет некоторые дополнения модели OpenConfig для IP интерфейсов";
revision "2017-03-17" {
description
"First release";
reference "1.0.0";
}
grouping secondary-top {
description "Дополнительная опция secondary";
leaf secondary {
type boolean;
default "false";
description
"Большниство платформ требуют опцию secondary, когда на одном интерфейсе настраивается несколько IPv4 адресов";
reference "https://www.cisco.com/c/en/us/td/docs/ios/12_2/ip/configuration/guide/fipr_c/1cfipadr.html#wp1001012";
}
}
augment "/oc-if:interfaces/oc-if:interface/" +
"oc-if:subinterfaces/oc-if:subinterface/" +
"oc-ip:ipv4/oc-ip:addresses/oc-ip:address/" +
"oc-ip:config" {
description "Добавить дополнительную опцию для IP адресов сабинтерфейсов";
uses secondary-top;
}
augment "/oc-if:interfaces/oc-if:interface/" +
"oc-vlan:routed-vlan/" +
"oc-ip:ipv4/oc-ip:addresses/oc-ip:address/" +
"oc-ip:config" {
description "Добавить дополнительную опцию для IP адресов SVI";
uses secondary-top;
}
}