熱門分類
載入中…
目錄0%

🧭 Juniper Junos 自動化設定同步:結合 GitLab Pipeline 部署範例

    🧭 Juniper Junos 自動化設定同步:結合 GitLab Pipeline 部署範例

    本文示範如何以 GitLab CI/CD 串接 Junos NETCONF + PyEZ,建立「Pull Request(Merge Request)→自動測試→人工核准→分段發佈→自動回滾」的安全流程,讓多台 Juniper 裝置的設定變更可被版本控管、審核與快速回復。

    📑 大綱

    一、流程設計與檔案結構

    config-as-code 方式管理:把 Junos 設定作為可版本化的文字,透過 MR 審核後觸發 Pipeline。

    repo/
    ├─ inventory/
    │  ├─ prod.yml               # 生產設備清單(hostname、mgmt_ip、site、批次分組)
    │  └─ lab.yml                # 測試環境
    ├─ configs/
    │  ├─ base/                  # 通用段落(AAA、syslog、ntp、snmp)
    │  ├─ site-tpl/              # 站點模板(jinja2)
    │  └─ devices/               # 每台設備要套的 overrides
    ├─ scripts/
    │  ├─ render.py              # 套模板→產出候選 config
    │  ├─ diff.py                # 取得差異(dry-run)
    │  ├─ deploy.py              # 提交、commit confirmed、驗證、confirm
    │  └─ rollback.py            # 失敗時回滾或載入最後穩定版
    ├─ .gitlab-ci.yml
    └─ README.md
    

    二、環境需求與準備

    • Junos 裝置開啟 NETCONF
      set system services netconf ssh
      set system login user cicd class super-user authentication plain-text-password
    • GitLab Runner(Shell 或 Docker),建議以 Docker image 內建 PyEZ:
      FROM python:3.11-slim
      RUN pip install junos-eznc jinja2 pyyaml
    • GitLab CI Variables(masked & protected):
      • JUNOS_USERJUNOS_PASS(或以 SSH key/私鑰)
      • TARGET_ENV(lab/prod)、BATCH(01/02...)

    三、PyEZ 連線與差異比對(dry-run)

    以 PyEZ 產生 candidate config,比對差異但不提交。

    scripts/diff.py
    #!/usr/bin/env python3
    import yaml, sys, time
    from jnpr.junos import Device
    from jnpr.junos.utils.config import Config
    from lxml import etree
    
    def load_inventory(path):
        with open(path) as f:
            return yaml.safe_load(f)
    
    def show_diff(host, user, passwd, cfg_path):
        dev = Device(host=host, user=user, passwd=passwd, gather_facts=False, normalize=True)
        dev.open()
        cu = Config(dev)
        with open(cfg_path) as f:
            config_text = f.read()
        cu.lock()
        try:
            cu.load(config_text, format="text", merge=False)   # 以 replace 或 set/merge 視你的模板
            diff = cu.diff()
            print(f"===== DIFF @ {host} =====")
            print(diff if diff else "No changes.")
            cu.rollback()
        finally:
            cu.unlock()
            dev.close()
    
    if __name__ == "__main__":
        inv = load_inventory(sys.argv[1])  # e.g. inventory/lab.yml
        user = sys.argv[2]; passwd = sys.argv[3]
        for d in inv["devices"]:
            show_diff(d["mgmt_ip"], user, passwd, d["rendered_config"])
    

    四、GitLab CI Pipeline:lint → dry-run → deploy → rollback

    以下範例展示完整的 .gitlab-ci.yml(可依專案裁剪)。

    .gitlab-ci.yml
    image: python:3.11-slim
    
    stages: [lint, render, dryrun, deploy, postcheck, rollback]
    
    before_script:
      - pip install junos-eznc jinja2 pyyaml
    
    variables:
      PYTHONUNBUFFERED: "1"
      TARGET_ENV: "lab"   # 可由 MR 或觸發參數覆寫
      BATCH: "01"
    
    lint:
      stage: lint
      script:
        - python -m py_compile scripts/*.py
        - python -c "import yaml; import glob; [yaml.safe_load(open(f)) for f in glob.glob('inventory/*.yml')]"
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    
    render:
      stage: render
      script:
        - python scripts/render.py inventory/${TARGET_ENV}.yml  # 產生 devices.*.conf
      artifacts:
        paths: [out/]
        expire_in: 1 week
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "main"'
    
    dry-run:
      stage: dryrun
      needs: [render]
      script:
        - python scripts/diff.py inventory/${TARGET_ENV}.yml "$JUNOS_USER" "$JUNOS_PASS"
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      allow_failure: false
    
    deploy:
      stage: deploy
      needs: [render]
      script:
        - python scripts/deploy.py inventory/${TARGET_ENV}.yml "$JUNOS_USER" "$JUNOS_PASS" "$BATCH"
      rules:
        - if: '$CI_COMMIT_BRANCH == "main"'
          when: manual           # 需人工按鈕核准
          allow_failure: false
    
    postcheck:
      stage: postcheck
      needs: [deploy]
      script:
        - python scripts/postcheck.py inventory/${TARGET_ENV}.yml "$JUNOS_USER" "$JUNOS_PASS"
      rules:
        - if: '$CI_COMMIT_BRANCH == "main"'
      allow_failure: false
    
    rollback:
      stage: rollback
      needs: [deploy]
      when: on_failure
      script:
        - python scripts/rollback.py inventory/${TARGET_ENV}.yml "$JUNOS_USER" "$JUNOS_PASS"
      rules:
        - if: '$CI_COMMIT_BRANCH == "main"'
    
    scripts/deploy.py(含 commit confirmed)
    #!/usr/bin/env python3
    import yaml, sys, time
    from jnpr.junos import Device
    from jnpr.junos.utils.config import Config
    
    CONFIRM_SEC = 120   # 兩分鐘自動回滾
    
    def batch(devices, batch_tag):
        return [d for d in devices if str(d.get("batch","01")).zfill(2) == str(batch_tag).zfill(2)]
    
    def deploy_one(host, user, passwd, cfg_path):
        dev = Device(host=host, user=user, passwd=passwd, gather_facts=False, normalize=True)
        dev.open()
        cu = Config(dev)
        cu.lock()
        try:
            with open(cfg_path) as f:
                cfg = f.read()
            cu.load(cfg, format="text", merge=False)
            if not cu.diff():
                print(f"[{host}] No changes, skip.")
                cu.rollback()
                return True
            cu.commit_check()
            cu.commit(confirm=CONFIRM_SEC)     # 先 commit confirmed
            # TODO: 這裡可放健康檢查(snmp/route/bgp/if)
            time.sleep(5)
            cu.commit(comment="CI confirm")    # 確認生效,避免自動回滾
            print(f"[{host}] Deployed OK.")
            return True
        except Exception as e:
            print(f"[{host}] ERROR: {e}")
            cu.rollback()
            return False
        finally:
            cu.unlock()
            dev.close()
    
    if __name__ == "__main__":
        inv = yaml.safe_load(open(sys.argv[1]))
        user = sys.argv[2]; passwd = sys.argv[3]; batch_tag = sys.argv[4]
        targets = batch(inv["devices"], batch_tag)
        ok = all(deploy_one(d["mgmt_ip"], user, passwd, d["rendered_config"]) for d in targets)
        sys.exit(0 if ok else 1)
    

    五、多裝置分批部署與失敗回滾

    • 分批策略:以 inventory.prod.yml 中的 batch: 01/02/03... 控制,每批成功後再人工觸發下一批。
    • 回滾策略
      1. 優先使用 commit confirmed 自動回滾(未 confirm 即還原)。
      2. 保留 load rescuerollback 1;必要時以 rollback.py 對多台批次回復。
    scripts/rollback.py
    #!/usr/bin/env python3
    import yaml, sys
    from jnpr.junos import Device
    from jnpr.junos.utils.config import Config
    
    def rb(host, user, passwd):
        dev = Device(host=host, user=user, passwd=passwd, gather_facts=False)
        dev.open()
        cu = Config(dev)
        cu.lock()
        try:
            cu.rollback(1)   # 回上一版;或 cu.load('rollback 1', format='text')
            cu.commit(comment="CI auto rollback")
            print(f"[{host}] rolled back.")
            return True
        except Exception as e:
            print(f"[{host}] rollback failed: {e}")
            return False
        finally:
            cu.unlock(); dev.close()
    
    if __name__ == "__main__":
        inv = yaml.safe_load(open(sys.argv[1]))
        user = sys.argv[2]; passwd = sys.argv[3]
        ok = all(rb(d["mgmt_ip"], user, passwd) for d in inv["devices"])
        sys.exit(0 if ok else 1)
    

    六、營運細節:密鑰、審核與稽核

    • 憑證與密碼:以 GitLab Protected & Masked variables 或 Vault;建議改用 SSH key / 設備本機憑證帳號。
    • 權限:Runner 僅允許對特定 branch(main)執行 deploy;MR 需至少 1–2 位 Reviewer。
    • 稽核追蹤:保存 Pipeline artifacts(diff 與最終 config),每次發布附上變更摘要與工單編號。
    • 健檢腳本:可在 postcheck 段落以 RPC 撈介面狀態、BGP/OSPF 鄰居數、路由計數並回報。

    七、常見問題與排錯

    • 🔌 連不上 NETCONF:確認 set system services netconf ssh 與管理 VRF/ACL 放行。
    • 🧱 commit-check 失敗:以 show system commitshow | compare 查語法與相依。
    • ⏳ confirmed 超時回滾:確認 postcheck 成功後要 commit 二次以移除確認計時。
    • 🔐 變數外洩風險:Runner 日誌不要輸出敏感資訊;使用 --mask 或避免 print(pass)

    🧭 行動清單

    ✅ 建立 inventory 與模板,導入既有 Junos config 至「可比對」的文字
    ✅ 配置 GitLab CI Variables(JUNOS_USER / JUNOS_PASS / TARGET_ENV / BATCH)
    ✅ 開啟裝置 NETCONF,測試 scripts/diff.py 是否能產生差異
    ✅ 以 MR 觸發 lint/dry-run,審核後手動執行 deploy 批次
    ✅ 驗證 postcheck,並於需要時一鍵 rollback
      

    🔗 延伸閱讀

    — WWFandy・網路自動化筆記

    💬 你的做法是?

    你在 Junos 自動化會選 Ansible、PyEZ 還是 Salt?遇過哪些坑?歡迎在下方留言分享你的實戰經驗!

    🔗 分享這篇 LINE Facebook X

    沒有留言:

    字級