็†ฑ้–€ๅˆ†้กž
 ่ผ‰ๅ…ฅไธญ…
็›ฎ้Œ„

๐Ÿงญ 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 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 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

    ๆฒ’ๆœ‰็•™่จ€:

    ๅผต่ฒผ็•™่จ€

    ๅญ—็ดš