Nəzəri əsaslar və memarlıq

Operator səviyyəli konfiqurasiya generasiyası Python, Jinja2 şablonlaşdırıcısı və çatdırma kitabxanaları (Scrapli, Netmiko, NAPALM, Nornir) üzərində qurulur. Infrastructure as Code (IaC) prinsipi məlumatı, şablonu və nəqliyyatı ayırır: 1. Xidmət məlumatları (Service Data) YAML və ya JSON-da saxlanılır (müştəri adı, RD, RT, CE prefikslər, QoS parametrləri) və render-dən əvvəl sxem (Pydantic, JSON Schema) və biznes qaydaları ilə yoxlanılır. 2. Jinja2 şablonu (.j2) konkret vendorun sintaksisini (Cisco, Huawei, Juniper, MikroTik) dəyişən əvəzləmələri ({{ vrf_name }}, {{ rd }}) və idarəetmə konstruksiyaları ({% for peer in peers %}, {% if vlan %}) ilə saxlayır; StrictUndefined boş dəyərin render olunmasına imkan vermir. 3. Python skripti konfiqurasiyanı render edir və çatdırır: Netmiko və Scrapli CLI sətirlərini tranzaksiyasız göndərir (nəticə yoxlanmalıdır), NAPALM load_merge_candidate, compare_config, commit_config və rollback-u yalnız dəstəklənən draylerlər üçün təklif edir (məsələn, ios və junos; Huawei VRP və RouterOS üçün əsas paketdə draylerlər yoxdur), Nornir tapşırıqları paralel icra edir. Bu yanaşma yüzlərlə oxşar blokda səhvləri aradan qaldırır, lakin məlumat yoxlamasını, tətbiqdən əvvəl diff-i və post-yoxlamaları əvəz etmir.

Həqiqət mənbəyi şablondan ayrıdır

PE üçün Infrastructure as Code yanaşması üç şeyin ayrılmasına əsaslanır: məlumat (nə konfiqurasiya olunmalıdır), şablon (bu konkret platformada necə yazılır) və nəqliyyat (necə çatdırılır). Məlumat niyyəti təsvir edir: A müştərisi, RT 65000:100, PE-CE interfeysi. O, Git-də və ya NetBox-da YAML/JSON şəklində saxlanılır. Şablon hər istehsalçı üçün biznes məntiqsiz Jinja2 faylıdır. Nəqliyyat Netmiko, NAPALM, Scrapli və ya Nornir-dir. Bir niyyət dörd sintaksisə render olunur, cihazlarda isə əl ilə düzəliş olmur.

yaml
# 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) düsturu ilə yığılır və hər PE-də unikaldır; bu, bir VRF-nin prefikslərini iki PE-dən ayrıca elan etmək üçün lazımdır (multipath, sürətli konvergensiya).

Render-dən əvvəl məlumatların validasiyası

Məlumatlar sxemlə yoxlanılır: Pydantic (tiplər, RT və RD üçün ^\d+:\d+$ requlyar ifadəsi), JSON Schema və ya yamllint. Biznes qaydaları kodla yazılır: müştəri RT-si icazəli siyahıdadır, VRF adları unikaldır, RD PE daxilində unikaldır, interfeys PE-yə aiddir. Məlumat yanlışdırsa, şablon çağırılmamalıdır: bu mərhələdəki xəta sıfıra başa gəlir, cihazda isə insidentə.

Jinja2: konfiqurasiyalar üçün həqiqətən nə lazımdır

  • StrictUndefined: susmaya görə naməlum dəyişən boş sətir kimi render olunur və dəyərsiz route-target export alırsınız. StrictUndefined ilə render UndefinedError ilə sınır.
  • trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True: {% %} teqləri boş sətir və çəkinti qoymur. Konfiqurasiyalar üçün çəkinti vacibdir (Cisco və Huawei-də iç-içəlik boşluqlarla müəyyən olunur).
  • loop.index ilə dövrlər, sort(attribute="name") filtri (sabit çıxış sırası, deməli təmiz diff), məcburi olmayan sahələr üçün default([...]), join(","), selectattr, map, unique.
  • İfadələrdə arifmetika (pe_id + v.rd_offset), təkrarlanan bloklar üçün macro və include, dövr daxilində sayğaclar üçün namespace.
  • Konfiqurasiyalar üçün autoescape söndürülüb: bu HTML deyil. Lakin etibarsız mənbədən olan şablonları da render etmək olmaz; bunun üçün SandboxedEnvironment var.
  • Fərdi filtrlər (məsələn, RD yoxlaması) env.filters-də qeydiyyatdan keçirilir.

Render deterministik olmalıdır: eyni məlumat bayt-bayt eyni nəticə verir, əks halda CI-da diff küylü olur.

python
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)

Nəqliyyat: Netmiko, NAPALM, Scrapli, Nornir

  • Netmiko (ConnectHandler, cisco_xe, juniper_junos, huawei_vrpv8, mikrotik_routeros tipləri) CLI sətirlərini göndərir. O, tranzaksiyalar haqqında heç nə bilmir: error_pattern verilməyibsə, send_config_set % Invalid input zamanı dayanmır. Çıxışı yoxlayın.
  • NAPALM ios, junos və digər drayverlər üçün vahid load_merge_candidate, compare_config, commit_config, discard_config, rollback API-si verir; əsas paketdə Huawei VRP və RouterOS üçün drayver yoxdur. Junos-da commit_config(revert_in=...) commit confirmed-i həyata keçirir, IOS-XE-də isə geri qayıtma configure replace-ə əsaslanır.
  • Scrapli böyük parklarda Netmiko-dan sürətlidir və tipli drayverlərə malikdir; istehsalçı platformaları düzgün seçilməlidir (IOS-XE drayveri Junos-a uyğun gəlmir).
  • Nornir tapşırıqları paralel icra edir (num_workers), inventory-ni Python-da saxlayır və mürəkkəb ardıcıllıqlar üçün istifadə olunur: əvvəl canary, sonra qalanlar.

İdempotentlik və diff

Hər işə salınmada bütün konfiqurasiyanı göndərmək pisdir: loqları küylü edir və riski artırır. Fərqi hesablamaq lazımdır. Junos-da compare_config candidate diff-ini göstərir. IOS-XE-də render olunmuş sətirlər show running-config | section vrf definition ilə müqayisə olunur. RouterOS üçün müqayisə /export ilə aparılır. Diff boşdursa, cihaza heç nə göndərilmir: bu, skript səviyyəsində idempotentlikdir.