๐งญ 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?้้ๅชไบๅ?ๆญก่ฟๅจไธๆน็่จๅไบซไฝ ็ๅฏฆๆฐ็ถ้ฉ!
ๆฒๆ็่จ:
ๅผต่ฒผ็่จ