راهنمای شبکه، زیرساخت و امنیت سایبری

خودکارسازی BGP با NETCONF، YANG و پایتون (راهنمای عملی Cisco IOS-XE)

خودکارسازی BGP با NETCONF، YANG و پایتون

در این نوشته تجربه عملی خودم را از مهاجرت گردش‌کار برقراری هم‌نشینی BGP از حالت کاملاً دستی مبتنی بر CLI به یک چرخهٔ اتوماتیک مبتنی بر NETCONF و مدل‌های YANG بازگو می‌کنم. تمرکز اصلی بر بستر Cisco IOS-XE است و برای لایهٔ مدل‌داده از مدل باز OpenConfig BGP بهره می‌گیرم؛ زیرا قابلیت حمل میان نسخه‌ها و پلتفرم‌ها را حفظ می‌کند. مسئلهٔ محوری، جفت‌کردن دو دغدغهٔ همیشگی است: «سرعت در برقراری Session جدید» و «کنترل دقیق پیشوندهای مجاز روی هر Peering». راه‌حل من، تولید برنامه‌ای پیکربندی، اعتبارسنجی پیش از اعمال، Commit تأییدشونده با تایمر Rollback خودکار، و بازخوانی وضعیت عملیاتی پس از اعمال است. همهٔ قطعه‌کدها در قالب تصویرهای ضمیمه (Code Card) آمده‌اند تا مطابقت‌پذیری و کپی‌شدن بی‌قید و شرط آن‌ها در محیط تولید، پیش از بازبینی ممکن نشود. نتیجهٔ نهایی در سازمان ما کاهش زمان راه‌اندازی هر Peering از حدود چهل دقیقه به کمتر از سه دقیقه و حذف کامل خطاهای تایپی در Prefix-Listها بوده است.

چرا روش‌های سنتی پیکربندی BGP دیگر پاسخگو نیستند؟

من در محیط Enterprise/ISP Edge کار می‌کنم؛ جایی که هر هفته حداقل یک هم‌نشینی جدید با Customer، Partner یا Upstream اضافه می‌شود. گردش‌کار سنتی این‌گونه بود: یک همکار درخواست را در تیکت می‌نوشت، من به روتر SSH می‌زدم، AS Number و آدرس‌ها را از تیکت کپی می‌کردم، Prefix-List دستی می‌ساختم، آن را به Policy جهت ورود و خروج وصل می‌کردم و امیدوار بودم خطایی نکرده باشم. این فرایند سه آسیب جدی داشت. نخست، خطر انسانی: یک پیشوند اشتباه یا یک Leaky Route می‌تواند کل ترافیک یک مشتری را مختل یا مسیر ترانزیت را فاسد کند. دوم، عدم تکرارپذیری: هر روتر IOS-XE با نسخه و تاریخچهٔ پیکربندی متفاوت، رفتار کمی متفاوت نشان می‌دهد و نمی‌توان یک «Template» دقیق بدون ابزار مدل‌محور اعمال کرد. سوم، نبود Trace: هیچ سندی ساخت‌یافته از اینکه چه کسی، چه زمانی و با چه محتوایی پیکربندی را تغییر داده باقی نمی‌ماند. دامنهٔ من شامل روترهای لبهٔ Catalyst 8000 و ISR 4000 با IOS-XE 17.x، کانتینر اتوماسیون روی لینوکس، و مخزن Git برای نسخه‌بندی است. من از Cisco IOS-XE native YANG فقط برای مواردی استفاده می‌کنم که در مدل OpenConfig موجود نیست؛ در غیر این صورت همه‌جا OpenConfig را انتخاب می‌کنم.

مزیت‌های NETCONF و مدل‌داده YANG در مقایسه با دستورات سنتی CLI

من سال‌ها با Expect و Scraping CLI کار کردم و مشکلاتش را از نزدیک دیده‌ام: خروجی show دست‌نویس parse می‌شود، هر نسخهٔ IOS-XE ظاهر خروجی را کمی تغییر می‌دهد و اسکریپت‌ها شکننده‌اند. NETCONF برعکس، یک تراکنش RPC ساخت‌یافته روی SSH (پورت 830) است که داده‌ها را در قالب XML و بر اساس مدل YANG منتقل می‌کند. سه مزیت تعیین‌کننده برای من داشت. نخست، تراکنش واقعی: با Candidate Configuration می‌توانم تغییرات را در یک بخش جدا اعمال کنم، صحت‌سنجی کنم و سپس با Commit نهایی یا Discard آن را ببندم؛ این در CLI خام IOS-XE معادل ندارد. دوم، اعتبارسنجی ساخت‌یافته: سرور NETCONF اگر دادهٔ ارسالی با مدل YANG هم‌خوانی نداشته باشد، پیش از هر Commit خطای rpc-error با جزئیات دقیق (error-type، error-tag، error-path) برمی‌گرداند؛ یعنی بخش بزرگی از اعتبارسنجی روی دستگاه انجام می‌شود. سوم، اختصاصی‌بودن Schema: با تبادل Capability در Hello اولیه، دقیقاً می‌فهمم نسخهٔ موردنظر کدام مدل‌ها را پشتیبانی می‌کند و اگر دستگاه مدل OpenConfig BGP را نداشته باشد، پیش از ارسال پیکربندی می‌فهمم و راه جایگزین (Cisco-native IETF/native YANG) را انتخاب می‌کنم. البته NETCONF چربی‌یکی ندارد: سربار XML و Session SSH بالاست و برای Telemetry جریانی مبتنی بر gRPC هنوز بهتر عمل می‌کند؛ اما برای «اعمال تغییر کنترل‌شده» انتخاب اول من است.

طراحی معماری اتوماسیون شبکه با Git، پایتون و ابزارهای اعتبارسنجی

من یک Automation Host مستقل در DMZ مدیریت دارم که فقط از طریق SSH Key به شبکه وصل است. روی آن یک ریپو Git قرار دارد که «حقیقت واحد» (Single Source of Truth) است؛ هر پیکربندی BGP و Prefix-Set به‌صورت فایل YAML نسخه‌بندی‌شده و بازبینی‌شده (Pull Request با دو بازبین) در آن ذخیره می‌شود. یک Inventory سادهٔ YAML دستگاه‌ها را با مشخصات (آی‌پی، مدل، نسخه، Shipment) نگه می‌دارد. یک لایهٔ Generator از YAMLها Payloadهای OpenConfig می‌سازد و یک لایهٔ Runner با ncclient آنها را به دستگاه می‌فرستد. خط CI (در محیط من GitLab CI) پیش از Merge دو کار انجام می‌دهد: یک Dry-Run در برابر Sandbox روتر آزمایشی، و یک Diff بین Running فعلی و Candidate تولیدشده. هر Commit Git معادل یک «تغییر شبکه» است و می‌توانم با یک Revert هم تاریخ پیکربندی و هم خود پیکربندی را برگردانم. این معماری به من اجازه می‌دهد هیچ‌وقت مستقیم به Production روتر SSH نزنم؛ مگر برای موارد اضطراری که خودش لاگ و تیکت اجباری دارد.

لایه وظیفه اصلی در معماری من
Source of Truth (Git + YAML) نگهداری نسخه‌بندی‌شدهٔ پیکربندی BGP و Prefix-Set با بازبینی Pull-Request
Generator تبدیل YAML به Payload معتبر OpenConfig با pyangbind/pydantic
Runner (ncclient) ارسال edit-config با lock و commit confirmed و مدیریت Session
CI Pipeline Dry-Run روی Sandbox و Diff با Running پیش از Merge
Verify & Telemetry خواندن Session-State و Prefix Count و هشدار زودهنگام در Grafana

 

مقایسه مدل‌های داده OpenConfig و Cisco-native

در IOS-XE 17.x سه خانوادهٔ مدل در دسترس است: Cisco-native (دقیقاً منطبق با CLI)، استانداردهای IETF، و OpenConfig. تجربهٔ من این است که برای BGP مدل OpenConfig تمیزتر و قابل‌حمل‌تری دارد؛ ساختار آن برون‌مرکز (AFI/SAFI زیر هر Neighbor) و Policyهای Import/Export به‌صورت ارجاع به Routing-Policy تعریف می‌شود که دقیقاً همان چیزی است که برای Prefix-List لازم دارم. به‌جای دست‌ساز‌کردن XML، من از ابزارهایی مثل pyangbind یا pydantic-YAML استفاده می‌کنم تا اشیای پایتونی را به Payload تبدیل کنم و خطاهای Namespace را در زمان Build بگیرم. یک نکتهٔ سازگاری مهم در IOS-XE: هنگام Commit یک تغییر OpenConfig، IOS-XE آن را به CLI داخلی Translate می‌کند؛ گاهی این Translate یک Command اضافه می‌سازد یا ترتیب فرمان‌ها را عوض می‌کند. من همیشه پس از Commit یک get-config می‌گیرم و آن را با آنچه فرستادم Compare می‌کنم؛ اگر اختلاف ساختاری جزئی دیدم، در Report ثبتش می‌کنم ولی آن را «خطا» نمی‌گیرم، مگر این‌که رفتار شبکه تغییر کند. همچنین برای Neighborهای IPv6 بهتر است از مدل OpenConfig BGP در نسخه‌های اخیر IOS-XE استفاده کنم؛ در نسخه‌های قدیمی‌تر این مسیر با Cisco-native YANG قابل‌اعتمادتر بود.

<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
  <edit-config>
    <target><candidate/></target>
    <default-operation>merge</default-operation>
    <config>
      <bgp xmlns="http://openconfig.net/yang/bgp">
        <neighbors>
          <neighbor>
            <neighbor-address>10002</neighbor-address>
            <config>
              <peer-as>65002</peer-as>
              <description>PEERISPEDGE01</description>
              <enabled>true</enabled>
            </config>
            <afis-safis>
              <afi-safi>
                <afi-safi-name xmlns:ocbt="http://openconfig.net/yang/bgp-types">ocbt:IPV4_UNICAST</afi-safi-name>
                <config><enabled>true</enabled></config>
                <apply-policy>
                  <config>
                    <import-policy>PREFIXSETINV4</import-policy>
                    <export-policy>PREFIXSETOUTV4</export-policy>
                  </config>
                </apply-policy>
              </afi-safi>
            </afis-safis>
          </neighbor>
        </neighbors>
      </bgp>
    </config>
  </edit-config>
</rpc>

کارت کد ۱ — ماژول Generate و Validate: ساخت Payload مدل OpenConfig BGP و Prefix-Set از فایل YAML و اعتبارسنجی ساختاری و معنایی پیش از ارسال

۵ مرحله کلیدی در چرخه خودکارسازی BGP

چرخه من پنج مرحله دارد. در Generate، از YAML منبع، Payload OpenConfig می‌سازم؛ برای هر Peering دو بخش جدا دارم: Neighbor Config و Prefix-Set مربوط به آن. در Validate، سه لایهٔ صحت‌سنجی اجرا می‌کنم: اول ساختاری (JSON-Schema/YANG)، دوم معنایی (بررسی این‌که هر Prefix-List خالی نباشد و حداقل یک Default-Deny داشته باشد؛ نبود آن یعنی تلاش برای پذیرش «هر پیشوند» از مشتری و این یک ریسک امنیتی جدی است)، و سوم شبیه‌سازی روی روتر Sandbox. در Push، ابتدا با lock روی Candidate قفل انحصاری می‌گیرم تا هیچ تغییر موازی رخ ندهد، سپس edit-config را اعمال و با commit confirmed تایم‌دار Commit می‌کنم. در Verify، وضعیت Session و شمارش Prefixهای دریافتی را از Operational Data Store می‌خوانم؛ اگر بعد از حدود سی ثانیه Session برقرار نشده بود یا شمارش Prefix از حد معقول خارج بود، تلاش برای Confirm نمی‌کنم. در Rollback، اگر Verify موفق نبود، می‌گذارم تایمر Confirmed-Commit تمام شود؛ دستگاه خودکار به پیکربندی پیشین برمی‌گردد و من با discard-changes بخش Candidate را هم خالی می‌کنم. این مرحلهٔ آخر به من اطمینان می‌دهد که حتی اگر اتصال من قطع شود، دستگاه در وضعیت ناپایدار نمی‌ماند.

import logging
from ncclient import manager
from ncclient.operations import RPCError

LEASE_TIMEOUT = 600  # rollback timer optimeout sec

def push_device(host, user, pkey, cfg):
    build_edit_config_device(OpenConfig_payload)
    with manager.connect(
        host=host,
        port=830,
        username=user,
        key_filename=pkey,
        hostkey_verify=False,
        device_params={'name': 'csr'},
        timeout=TIMEOUT
    ) as m:
        m.lock('candidate')  # exclusive writer
        for chunk in cfg:
            m.edit_config(config=chunk, target='candidate')
            
        # commit confirmed: auto-rollback after LEASE if unconfirmed
        m.commit_confirmed(True, timeout=str(LEASE))
        try:
            assert verify_bgp_device(m), 'verify failed'
            m.commit_confirmed(False)  # make it permanent
        except (RPCError, AssertionError) as e:
            logging.error(f'rollback: %s', e)
            m.discard_changes()  # revert candidate
        finally:
            m.unlock('candidate')

def verify_bgp_device(m):
    filt = '<bgp xmlns="http://openconfig.net/yang/bgp"><neighbors/></bgp>'
    r = m.get_filter(filt, timeout=TIMEOUT)
    return is_peer_established(r.data, device)

کارت کد ۲ — ماژول Push: گرفتن lock، اعمال edit-config روی Candidate و Commit تأییدشونده با تایمر ۶۰۰ ثانیه‌ای

راهکارهای امنیتی و کنترل ریسک برای جلوگیری از قطعی شبکه در تولید

من قواعد سخت‌گیرانه‌ای برای کنترل ریسک گذاشته‌ام. نخست، همیشه قبل از هر تغییر lock می‌گیرم؛ اگر قفل در دست دیگری بود، کار را لغو می‌کنم و به‌جای Force، انتظار می‌کشم. دوم، همه‌چیز روی Candidate اعمال می‌شود نه روی Running؛ Commit مستقیم روی Running برای من ممنوع است. سوم، Commit تأییدشونده با تایمر ۶۰۰ ثانیه‌ای؛ این کار یک «Net» ایمنی است که اگر فرایند Verify در میانه از کار بیفتد، دستگاه خودش به وضعیت قبل برمی‌گردد. چهارم، Idempotency: هر Payload با یک شناسهٔ یکتا (Message-ID ثابت بر اساس Hash محتوا) ساخته می‌شود؛ اگر همان محتوا دوباره اجرا شود، دستگاه تغییر معناداری نمی‌بیند و Diff خالی است. پنجم، Rate-Limit: بیش از یک Commit هم‌زمان روی یک دستگاه ندارم و یک صف برای تغییرات می‌سازم؛ فشار هم‌زمان چند Commit به روترهای لبهٔ پرترافیک تجربهٔ ناخوشایندی بود که از آن پرهیز می‌کنم. علاوه بر این‌ها، همهٔ این مراحل را لاگ ساخت‌یافته می‌کنم (زمان، دستگاه، هش محتوا، نتیجهٔ Verify) تا برای Audit آماده باشد.

سازوکار ایمنی اثر عملیاتی آن در محیط تولید
Lock روی Candidate جلوگیری از تغییر موازی و تعارض Sessionها
Commit Confirmed (۶۰۰ ثانیه) Rollback خودکار در صورت شکست Verify یا قطع اتصال
Idempotent Message-ID اجرای تکراری بدون تغییر معنادار و Diff خالی
Rate-Limit / صف تغییرات جلوگیری از فشار هم‌زمان چند Commit به روتر لبه
لاگ ساخت‌یافته Audit‌پذیری کامل زمان، دستگاه، هش محتوا و نتیجهٔ Verify

 

مانیتورینگ زنده وضعیت BGP

بعد از Commit موفق، من وضعیت عملیاتی را می‌خوانم؛ نه فقط پیکربندی. با یک GET روی زیردرخت OpenConfig BGP، فیلد Session-State هر Neighbor را بررسی می‌کنم و انتظار ESTABLISHED را دارم. برای پیشوندهای دریافتی، در IOS-XE از مسیر Operational (مثلاً شمارش accepted/filtered prefixes) استفاده می‌کنم و آن را با مقدار موردانتظار از DB مقایسه می‌کنم. برای پایش بلندمدت، روی دستگاه‌هایی که پشتیبانی می‌کنند، OpenConfig Telemetry با gRPC Dial-Out فعال می‌کنم تا تغییر شمارش پیشوندها یا افت Session را در Grafana ببینم؛ این دید، پیش از آن‌که مشتری زنگ بزند، به من هشدار می‌دهد. لاگ Session NETCONF و Syslog روتر هم به Elasticsearch می‌رود؛ با یک Query ساده می‌توانم پاسخ بدهم که «دیشب ساعت ۲۳ چه تغییری روی RTR-EDGE-02 رفت؟».

import difflib
import yaml
from xml.etree import ElementTree as ET

NS = 'http://openconfig.net/yang/routing-policy'
IRR_EXPORT = yaml.safe_load(open('irr_as65100.yml'))
# route-objects: ['203.0.113.0/24', '198.51.100.0/24', ...]

def build_prefix_set(name, prefixes):
    cfg = ET.Element('routing-policy', xmlns=NS)
    sets = ET.SubElement(cfg, 'defined-sets')
    ps_sets = ET.SubElement(sets, 'prefix-sets')
    ps = ET.SubElement(ps_sets, 'prefix-set')
    ET.SubElement(ps, 'name').text = name
    pre = ET.SubElement(ps, 'prefixes')
    for pfx in prefixes:
        net, mlen = pfx.split('/')
        p = ET.SubElement(pre, 'prefix')
        ET.SubElement(p, 'ip-prefix').text = net
        ET.SubElement(p, 'masklength-range').text = mlen
    return cfg

def dry_run(current, candidate):
    out = ET.tostring(candidate, encoding='unicode')
    return '\n'.join(difflib.unified_diff(
        current.splitlines(),
        out.splitlines(),
        fromfile='running', tofile='candidate', lineterm=''
    ))

def main():
    pfx = IRR_EXPORT['route-objects']
    cand = build_prefix_set('PREFIX-SET-IN-V4', pfx)
    current = fetch_running_get_config_via_ncclient()

کارت کد ۳ — ماژول Verify و Rollback: خواندن وضعیت عملیاتی Session-State و شمارش Prefix، و بازگشت ایمن با discard-changes

اصول امنیت و مدیریت دسترسی در بستر اتوماسیون NETCONF

اتوماسیون بدون امنیت، یک متریکس جدید برای نفوذ است. من برای اکانت NETCONF از AAA داخلی TACACS+ استفاده می‌کنم و دسترسی را با NACM محدود کرده‌ام؛ اکانت اتوماسیون فقط اجازهٔ write روی مسیرهای OpenConfig BGP و Routing-Policy را دارد و read روی Operational Data Store؛ به‌هیچ‌وجه دسترسی Full Admin ندارد. احراز هویت با کلید عمومی SSH (نه Password) و با انقضای دوره‌ای کلید انجام می‌شود. هر Automation Host یک اکانت مستقل دارد تا اگر یکی به خطر افتاد، بتوانم فقط همان را قطع کنم. همچنین ACL روی پورت 830 گذاشته‌ام تا فقط از subnet مدیریت قابل‌دسترس باشد و Session NETCONF روی کانال SSH و درون یک VLAN مدیریت جدا حمل شود.

راهنمای عیب‌یابی خطاهای رایج NETCONF و روش حل آن‌ها

خطاهای رایجی که من با آن‌ها سروکار داشته‌ام و اقدام من در برابر هرکدام، در جدول زیر خلاصه شده است:

خطا (error-tag) علت و اقدام پیشنهادی من
operation-failed (application) ناهم‌خوانی داده با Schema؛ بازبینی Namespace و ساختار درخت و Validate با pyang
in-use / lock-denied Session دیگری قفل را دارد؛ شناسایی با get و در اضطرار Kill-Session با مستندسازی دلیل
invalid-value مقدار AFI/SAFI یا AS Number با Enum/Pattern مدل نمی‌خواند؛ بازخوانی دقیق مدل YANG
commit-failed Validation داخلی IOS-XE؛ بررسی وجود Policy ارجاع‌شده و خالی‌نبودن Prefix-List با Error-Info
timeout / connection-refused پورت 830 بسته یا NETCONF-YANG فعال نیست؛ بررسی netconf-yang روی IOS-XE
resource-denied سقف Session سرور؛ ایجاد صف روی سمت اتوماسیون

 

برای هر مورد، اقدام من این است که اول با discard-changes وضعیت را پاک کنم، بعد علت را بگیرم و در پایان با یک Test در Sandbox اطمینان حاصل کنم.

چک‌لیست نهایی برای پیاده‌سازی موفق اتوماسیون BGP در سازمان

اتوماسیون BGP با NETCONF/YANG و Python در بستر Cisco IOS-XE برای من از یک «بازی با ابزار» به یک الزام عملیاتی تبدیل شد. هزینهٔ اولیهٔ ساخت خط (Git، CI، Sandbox، Testing) قابل‌توجه است اما سودش در حذف خطاهای انسانی، کوتاه‌شدن زمان Provisioning، و Audit‌پذیری تغییرات، به‌سرعت جبران می‌شود. چک‌لیست من به‌شرح زیر است:

  1. NETCONF-YANG روی هر دستگاه IOS-XE فعال و Version-compatible باشد.
  2. OpenConfig BGP و Routing-Policy در Capability دستگاه موجود باشند.
  3. اکانت اتوماسیون با NACM محدود و کلید SSH اختصاصی داشته باشد.
  4. هر تغییر با lock و commit confirmed تایم‌دار روی Candidate اعمال شود.
  5. پیش از Commit، Prefix-Listهای ورودی خالی نباشند و Default-Deny داشته باشند.
  6. پس از Commit، Session-State و شمارش Prefix با Verify عملیاتی چک شود.
  7. Rollback خودکار با تایمر Confirmed-Commit همیشه فعال باشد.
  8. همهٔ تغییرات در Git نسخه‌بندی و با Pull-Request بازبینی شوند.
  9. Telemetry برای هشدار زودهنگام روی Session و Prefix Count فعال باشد.
  10. لاگ‌ها ساخت‌یافته و قابل‌جست‌وجو نگه‌داری شوند.

یادداشت پایانی تجربه‌محور

اگر بخواهم تجربهٔ خودم را در یک جمله خلاصه کنم، می‌گویم: اتوماسیون شبکه بیشتر از آن‌که پروژهٔ ابزارسازی باشد، پروژهٔ مهندسی فرایند است. ابزارهای NETCONF، YANG، ncclient و Python در دسترس همه هستند؛ اما آن‌چه تفاوت ایجاد می‌کند، انضباط گردش‌کار است: نسخه‌بندی تغییرات، بازبینی دو-نفره، Sandbox معتبر، Commit تأییدشونده، و Verification عملیاتی. من در ابتدای مسیر، اسکریپت‌هایی داشتم که مستقیم روی Running اعمال می‌کردند؛ یک بار همین بی‌احتیاطی باعث شد یک Prefix-List ورودی ناقص روی یکی از روترهای لبه بنشیند و برای چند دقیقه بخشی از مسیرهای یک مشتری از دست برود. از آن روز، هیچ تغییری بدون تایمر Rollback به Production نمی‌رود. برای تیم‌هایی که تازه شروع کرده‌اند، توصیهٔ من این است که با یک روتر آزمایشی و یک Peering کم‌ریسک شروع کنند، مدل OpenConfig BGP را با pyang باز کنند و ساختارش را به‌طور کامل بفهمند، و سپس مرحله‌به‌مرحله Generate، Validate، Push، Verify و Rollback را به‌صورت پنج ماژول جدا بسازند. جدا بودن این ماژول‌ها بعدها امکان تست واحد، جایگزینی اجزا و عیب‌یابی سریع را فراهم می‌کند. خوانندهٔ علاقه‌مند می‌تواند برای جزئیات Payloadها به سه Code Card ضمیمه‌شده در همین نوشته مراجعه کند؛ آن‌ها دقیقاً همان قطعاتی هستند که خط اتوماسیون ما هر روز با آن‌ها کار می‌کند و پس از چندین بازبینی، در محیط تولید پایدار شده‌اند.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *