Теоретические основы и архитектура
Генерация конфигураций операторского уровня строится на связке Python, шаблонизатора Jinja2 и библиотек доставки (Scrapli, Netmiko, NAPALM, Nornir). Принцип Infrastructure as Code (IaC) разделяет данные, шаблоны и транспорт: 1. Данные сервиса (Service Data) хранятся в YAML или JSON (имя клиента, RD, RT, префиксы CE, параметры QoS) и до рендера проверяются схемой (Pydantic, JSON Schema) и бизнес-правилами. 2. Шаблон Jinja2 (.j2) содержит синтаксис конкретного вендора (Cisco, Huawei, Juniper, MikroTik) с подстановкой переменных ({{ vrf_name }}, {{ rd }}) и управляющими конструкциями ({% for peer in peers %}, {% if vlan %}); режим StrictUndefined не даёт отрисовать пустое значение. 3. Скрипт Python рендерит конфигурацию и доставляет её: Netmiko и Scrapli отправляют CLI-строки без транзакций (результат нужно проверять), NAPALM даёт load_merge_candidate, compare_config, commit_config и rollback только для поддерживаемых драйверов (например, ios и junos; для Huawei VRP и RouterOS драйверов в основной поставке нет), Nornir запускает задачи параллельно. Такой подход устраняет опечатки в сотнях однотипных блоков, но не заменяет проверку данных, diff перед применением и пост-проверки.
Источник правды отдельно от шаблона
Подход Infrastructure as Code для PE строится на разделении трёх вещей: данные (что должно быть настроено), шаблон (как это записывается на конкретной платформе) и транспорт (как это доставляется). Данные описывают намерение: клиент A, RT 65000:100, интерфейс PE-CE. Их хранят в YAML/JSON в Git или в NetBox. Шаблон - Jinja2-файл на вендора, без бизнес-логики. Транспорт - Netmiko, NAPALM, Scrapli или Nornir. Одно и то же намерение рендерится в четыре синтаксиса, и ручных правок на устройствах нет.
# data/customers.yml (source of truth, no secrets)
asn: 65000
vrfs:
- {name: A, rt: "65000:100", rd_offset: 0, iface: ether2}
- {name: B, rt: "65000:200", rd_offset: 10, iface: ether3}
- {name: SS, rt: "65000:900", rt_import: ["65000:900", "65000:100"],
rd_offset: 20, iface: ether4}RD собирается формулой asn:(pe_id + rd_offset), и у каждого PE он уникален, что нужно для раздельного анонса префиксов с одного VRF на двух PE (multipath, быстрая сходимость).
Валидация данных до рендера
Данные проверяют схемой: Pydantic (типы, регулярное выражение ^\d+:\d+$ для RT и RD), JSON Schema или yamllint. Бизнес-правила пишут кодом: RT клиента входит в разрешённый список, имена VRF уникальны, RD уникален в пределах PE, интерфейс принадлежит PE. Если данные неверны, шаблон не должен вызываться: ошибка на этом этапе стоит ноль, на устройстве она стоит инцидент.
Jinja2: что действительно нужно для конфигураций
StrictUndefined: по умолчанию неизвестная переменная рендерится пустой строкой, и вы получитеroute-target exportбез значения. СоStrictUndefinedрендер падает сUndefinedError.trim_blocks=True,lstrip_blocks=True,keep_trailing_newline=True: теги{% %}не оставляют пустых строк и отступов. Для конфигураций отступ важен (в Cisco и Huawei вложенность определяется пробелами).- Циклы с
loop.index, фильтрыsort(attribute="name")(стабильный порядок вывода, а значит чистый diff),default([...])для необязательных полей,join(","),selectattr,map,unique. - Арифметика в выражениях (
pe_id + v.rd_offset),macroиincludeдля повторяющихся блоков,namespaceдля счётчиков внутри циклов. autoescapeдля конфигураций выключен: это не HTML. Но и шаблоны из недоверенного источника рендерить нельзя; для них естьSandboxedEnvironment.- Пользовательские фильтры (например, проверка RD) регистрируются в
env.filters.
Рендер должен быть детерминированным: одни и те же данные дают побайтово одинаковый результат, иначе diff в CI шумит.
import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined
env = Environment(
loader=FileSystemLoader("templates"),
undefined=StrictUndefined,
trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True,
)
data = yaml.safe_load(open("data/customers.yml"))
cfg = env.get_template("pe_ios.j2").render(**data, pe_id=1)Транспорт: Netmiko, NAPALM, Scrapli, Nornir
- Netmiko (
ConnectHandler, типыcisco_xe,juniper_junos,huawei_vrpv8,mikrotik_routeros) отправляет CLI-строки. Он не знает про транзакции:send_config_setпо умолчанию не прерывается на% Invalid input, если не заданerror_pattern. Проверяйте вывод. - NAPALM даёт единый API
load_merge_candidate,compare_config,commit_config,discard_config,rollbackдля драйверовios,junosи других; для Huawei VRP и RouterOS в основной поставке драйверов нет. На Junoscommit_config(revert_in=...)реализует commit confirmed, а на IOS-XE откат основан наconfigure replace. - Scrapli быстрее Netmiko на больших парках и имеет типизированные драйверы; вендорские платформы нужно выбирать правильно (драйвер IOS-XE не подходит для Junos).
- Nornir запускает задачи параллельно (
num_workers), хранит inventory на Python и применяется для сложных последовательностей: canary, затем остальные.
Идемпотентность и diff
Пуш всего конфига при каждом запуске плох: он зашумляет логи и повышает риск. Нужно вычислять разницу. На Junos compare_config показывает diff candidate. На IOS-XE сравнивают отрисованные строки с show running-config | section vrf definition. Для RouterOS сравнивают с /export. Если diff пуст, на устройство ничего не отправляется: это и есть идемпотентность на уровне скрипта.