🧭 Juniper Junos 自動化設定同步:結合 GitLab Pipeline 部署範例
本文示範如何以 GitLab CI/CD 串接 Junos NETCONF + PyEZ,建立「Pull Request(Merge Request)→自動測試→人工核准→分段發佈→自動回滾」的安全流程,讓多台 Juniper 裝置的設定變更可被版本控管、審核與快速回復。
📑 大綱
- 一、流程設計與檔案結構
- 二、環境需求與準備
- 三、PyEZ 連線與差異比對(dry-run)
- 四、GitLab CI Pipeline:lint → dry-run → deploy → rollback
- 五、多裝置分批部署與失敗回滾
- 六、營運細節:密鑰、審核與稽核
- 七、常見問題與排錯
一、流程設計與檔案結構
以 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_USER、JUNOS_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...控制,每批成功後再人工觸發下一批。 - 回滾策略:
- 優先使用
commit confirmed自動回滾(未 confirm 即還原)。 - 保留
load rescue或rollback 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 commit、show | 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
🔗 延伸閱讀
- 🛣 Juniper 路由基礎:RIP/RIP v2 設定指南
- 📦 Juniper DHCP 伺服器設定(Junos OS)
- 🧭 Juniper 基本環境與常用指令整理
- 🐍 Python + GitLab API:備份與自動化實務
— WWFandy・網路自動化筆記
💬 你的做法是?
你在 Junos 自動化會選 Ansible、PyEZ 還是 Salt?遇過哪些坑?歡迎在下方留言分享你的實戰經驗!
沒有留言: