Junos PyEZ: Automate Junos Switches with Python - 夜莺博客

Junos PyEZ: Automate Junos Switches with Python

CLI scraping breaks the moment a vendor changes output formatting. Junos PyEZ is the officially supported Python library (package name junos-eznc) that talks to Junos over NETCONF instead of screen scraping, so you get structured XML/JSON replies, typed exceptions and a commit model that matches the device. This guide covers the parts you actually need in production: enabling NETCONF, connecting safely, reading facts and tables, running RPCs, and loading plus committing configuration with a diff.

What PyEZ Is and What It Needs

PyEZ is built on ncclient and speaks NETCONF to the device. On the Junos side, NETCONF over SSH must be enabled before anything works:

set system services netconf ssh
commit

Install the library on your automation host (Python 3):

pip install junos-eznc
python3 -c "import jnpr.junos; print(jnpr.junos.__version__)"

Install it inside a virtual environment and pin the version in requirements.txt. The library tracks Junos releases closely and a silent upgrade has broken more automation jobs than any device-side change:

python3 -m venv /opt/auto/venv
. /opt/auto/venv/bin/activate
pip install "junos-eznc==2.7.1" ncclient lxml jinja2 pyyaml
pip freeze > requirements.txt

Two facts about the connection model matter. First, Device.auto_probe makes open() probe NETCONF reachability with a timeout before it tries to establish the session, which turns a 30-second TCP hang into a fast, clear error. Second, PyEZ raises distinct exceptions per failure class: ConnectAuthError, ConnectRefusedError (NETCONF not enabled), ConnectTimeoutError, plus RpcError, ConfigLoadError and CommitError during changes.

Restrict NETCONF on the device side too - an open port 830 on a management interface is a login surface, not a feature:

set firewall family inet filter AUTOMATION term netconf from source-address 10.10.99.0/24
set firewall family inet filter AUTOMATION term netconf from protocol tcp
set firewall family inet filter AUTOMATION term netconf from destination-port netconf
set firewall family inet filter AUTOMATION term netconf then accept
set firewall family inet filter AUTOMATION term else then discard
set interfaces lo0 unit 0 family inet filter input AUTOMATION

Connect and Read Facts

from jnpr.junos import Device
from pprint import pprint

dev = Device(host='10.10.10.11', user='automation', password='secret', auto_probe=10)
dev.open()
pprint(dev.facts)          # hostname, model, junos version, serial number, uptime
dev.close()

Use the context manager form so the NETCONF session is always closed, even on exceptions:

with Device(host='10.10.10.11', user='automation', password='secret') as dev:
    print(dev.facts['hostname'], dev.facts['model'], dev.facts['version'])

In production, authenticate with a key rather than a password, and keep credentials out of the script entirely. PyEZ accepts an SSH private key file, or it will read ~/.ssh/config for the alias you pass as the host:

with Device(host='core1', user='automation',
            ssh_private_key_file='/home/auto/.ssh/id_ed25519',
            auto_probe=10) as dev:
    print(dev.facts['hostname'], dev.facts['version'], dev.facts['uptime'])

Facts are gathered lazily on first access, so a script that only needs the serial number does not pay for the full fact collection. Add gather_facts=False when you want to skip it deliberately and issue your own RPC instead.

Run Operational RPCs Instead of Parsing CLI

Anything that starts with show in the CLI has an RPC method. PyEZ converts the command name: show interfaces becomes dev.rpc.get_interface_information().

# structured, no text parsing
rsp = dev.rpc.get_interface_information(terse=True)
for intf in rsp.findall('.//interface-information/physical-interface'):
    name = intf.findtext('name')
    admin = intf.findtext('admin-status')
    oper = intf.findtext('oper-status')
    print(name, admin, oper)

# last resort: raw CLI text through the RPC channel
out = dev.rpc.cli('show configuration | display set', format='text')
print(out.text)

The conversion rule is mechanical: dashes become underscores, and any argument in the CLI command becomes a keyword argument of the same name. show bgp summary becomes dev.rpc.get_bgp_summary_information(), and show configuration interfaces ge-0/0/1 becomes a filtered get_config call that returns only the subtree you asked for:

cfg = dev.rpc.get_config(filter_xml={'name': 'interfaces'},
                         options={'inherit': 'inherit'})
print(dev.rpc.get_software_information().findtext('.//junos-version'))
print(dev.rpc.get_route_information(table='inet.0',
                                    destination='10.10.0.0/16'))

Filtering server-side with filter_xml matters on large configurations: pulling the whole config over NETCONF to grep one stanza in Python transfers megabytes and takes seconds, while the filtered call returns a few kilobytes.

Tables and Views: Turn XML Into Python Objects

For repeated inventory work, define a YAML Table/View once and get a list of dictionaries back - much cleaner than looping over XML in every script. The YAML maps RPC fields onto attribute names, and PyEZ does the parsing:

# interfaces.yml
InterfaceTable:
  rpc: get-interface-information
  args:
    terse: True
  item: physical-interface
  key: name
  view: InterfaceView

InterfaceView:
  fields:
    name: name
    admin: admin-status
    oper: oper-status
    desc: description
from jnpr.junos.factory.factory_loader import FactoryLoader
import yaml

with open('interfaces.yml') as f:
    globals().update(FactoryLoader().load(yaml.safe_load(f)))

with Device(host='core1', user='automation',
            ssh_private_key_file='/home/auto/.ssh/id_ed25519') as dev:
    table = InterfaceTable(dev)
    table.get()
    for item in table:
        print(item.name, item.admin, item.oper, item.desc)
    print(table.to_json())          # the same data as a JSON string

Once the table exists, an interface audit is a loop over objects with attributes, and the same YAML file can be reused by every script in the team. Version it with the code: the field names are device-specific and change with Junos versions.

Load, Diff and Commit Configuration

Configuration changes should always be visible before they hit the running config. Load into the candidate config, inspect the diff, then commit:

from jnpr.junos import Device
from jnpr.junos.utils.config import Config

with Device(host='10.10.10.11', user='automation', password='secret') as dev:
    with Config(dev, mode='exclusive') as cu:
        cu.load('set interfaces ge-0/0/1 description "uplink to core"', format='set')
        print(cu.diff())       # what would change
        if cu.commit_check():  # syntax + semantic check only
            cu.commit(comment='description update via PyEZ')
        else:
            cu.rollback()

Three habits make this safe on real devices:

  • commit_check() validates without changing the running config - catch syntax errors here, not at 2 a.m.
  • Always pass a comment to commit(); it lands in show system commit and makes the change auditable.
  • cu.rollback(1) reverts to the previous committed config, which is the fastest way to undo a bad deployment.

For changes to a remote or transit device, use a confirmed commit. Junos rolls the configuration back automatically if you do not confirm it in time, which turns a lockout into a five-minute inconvenience:

with Config(dev, mode='exclusive') as cu:
    cu.load(cfg_text, format='set', merge=True)
    print(cu.diff())
    cu.commit(comment='confirmed push, auto-rollback in 5 min', confirmed=True, timeout=5)
    # ... verify management access is still working ...
    cu.commit()          # confirm and keep the change
    # cu.rollback()      # or abort, if the change broke something

Configuration Templates With Jinja2

Templates are where PyEZ stops being a scripting library and starts replacing manual work. Render a Jinja2 template with the values that differ per device, load it as text or set format, and the commit path is unchanged:

# templates/vlan.j2
{% for vlan in vlans %}
set vlans {{ vlan.id }} vlan-id {{ vlan.id }}
set vlans {{ vlan.id }} description "{{ vlan.name }}"
{% endfor %}
from jinja2 import Environment, FileSystemLoader
from jnpr.junos import Device
from jnpr.junos.utils.config import Config

env = Environment(loader=FileSystemLoader('templates'), trim_blocks=True)
payload = env.get_template('vlan.j2').render(
    vlans=[{'id': 120, 'name': 'SERVERS'}, {'id': 130, 'name': 'STORAGE'}])

with Device(host='sw1', user='automation',
            ssh_private_key_file='/home/auto/.ssh/id_ed25519') as dev:
    with Config(dev, mode='exclusive') as cu:
        cu.load(payload, format='set', merge=True)
        if cu.diff():
            print(cu.diff())
            cu.commit(comment='vlan 120/130 via template', confirmed=True, timeout=5)
            cu.commit()

The if cu.diff(): guard is the habit that keeps template-driven automation from generating empty commits across a thousand devices - scheduled jobs that commit nothing still write commit history and still consume CPU on the RE.

Working Across Many Devices

Parallelism is what makes automation worth the setup. PyEZ sessions are network-bound, so a thread pool with a bounded worker count is enough; there is no need for async rewrites to push facts from a hundred devices:

from concurrent.futures import ThreadPoolExecutor
from jnpr.junos import Device
from jnpr.junos.exception import ConnectError

def facts(host):
    try:
        with Device(host=host, user='automation', auto_probe=10,
                    ssh_private_key_file='/home/auto/.ssh/id_ed25519') as dev:
            return host, dev.facts['model'], dev.facts['version']
    except ConnectError as e:
        return host, 'UNREACHABLE', str(e)

with ThreadPoolExecutor(max_workers=16) as pool:
    for host, model, version in pool.map(facts, inventory):
        print(f'{host:20} {model:16} {version}')

Keep max_workers modest - Junos management planes throttle aggressively and 200 simultaneous NETCONF sessions look like a denial-of-service attack to a small RE. Sixteen to thirty-two workers is a sane range for a mid-size fleet, and auto_probe ensures an unplugged device fails fast instead of holding a worker for the TCP timeout.

One more detail worth knowing before you build a fleet-wide loop: PyEZ sessions are per-device objects and are not thread-safe, so never share one Device between workers. Create a session inside the worker, use it, and close it - the cost of a NETCONF handshake is measured in milliseconds and is paid back the moment one hung device would otherwise block the rest of the run.

Error Handling Pattern

from jnpr.junos.exception import ConnectError, RpcError, ConfigLoadError, CommitError

try:
    with Device(host=host, user=user, password=pw, auto_probe=10) as dev:
        with Config(dev) as cu:
            cu.load(cfg_text, format='set', merge=True)
            cu.commit(comment='ansible-free push')
except ConnectError as e:
    print('cannot reach or authenticate:', e)
except ConfigLoadError as e:
    print('config rejected:', e)
except CommitError as e:
    print('commit failed, device rolled back:', e)

Catch the specific classes and keep the broad except Exception for the top of the script only. A CommitError means the candidate configuration was discarded and the device is running the previous configuration - a very different situation from a ConnectError, and the incident response differs accordingly.

Operational Checklist

  1. Enable system services netconf ssh and restrict it with a firewall filter to your automation subnets.
  2. Use a dedicated automation account with the minimum login class that allows configuration, not a super-user shared account.
  3. Set auto_probe so unreachable devices fail fast in large parallel runs.
  4. Always diff before commit, always comment on commit.
  5. Keep the PyEZ version pinned in requirements.txt — the library tracks Junos releases closely.
  6. Use confirmed commits with a timeout whenever the change touches management access or routing.
  7. Wrap device access in a context manager so a failed script cannot leak NETCONF sessions.

相关阅读:Junos 排障常用命令、Netmiko Python 自动化 以及 NAPALM getters 与配置比对。

原文链接:Junos PyEZ documentation