Skip to main content

junos-ops

PyPI version CI Python

日本語版 / Japanese

A Python CLI to automate Juniper/JUNOS operations over NETCONF: model-aware upgrade, rollback, reboot, config push, and RSI/SCF collection.

RSI/SCF? RSI = request support information (the JTAC-style support snapshot), SCF = show configuration | display set. Collected files are saved with .rsi / .scf extensions — hence the shorthand used throughout this tool.

Features

  • Automatic device model detection and package mapping
  • Safe package copy via SCP with checksum verification
  • Pre-install package validation
  • Rollback support (model-specific handling for MX/EX/SRX)
  • Scheduled reboot with automatic config-drift detection and re-install
  • On-demand recovery snapshot (snapshot) to sync the alternate boot media (MX-focused), with an alternate-media safety guard
  • Parallel RSI (request support information) / SCF (show configuration | display set) collection
  • Pre-flight check subcommand: NETCONF reachability, local firmware checksum (device-less), and remote firmware checksum in one unified table
  • Arbitrary CLI command execution across hosts (show) with RpcTimeoutError retry
  • Configuration push with commit confirmed safety and post-commit health checks (ping, uptime NETCONF probe, or any CLI command)
  • Jinja2 template support for per-host configuration generation (details)
  • Tag-based host filtering (--tags) for AND-matched multi-site workflows
  • Local firmware directory (lpath) with ~ expansion
  • Dry-run mode (--dry-run) for pre-flight verification
  • Machine-readable JSON output (--json) for piping into jq / monitoring / Ansible
  • Parallel execution via ThreadPoolExecutor (--workers N)
  • INI-based host and package management

Table of Contents

Installation

Homebrew (macOS)

brew install shigechika/tap/junos-ops

Bottles are built for Apple Silicon only. On an Intel Mac the formula builds from source (cryptography, lxml, PyNaCl and bcrypt are compiled, which pulls in rust and takes around 10 minutes per upgrade) — use pipx there instead.

pipx

Installs from PyPI into an isolated virtualenv using pre-built wheels, so no compiler toolchain is needed:

pipx install junos-ops
pipx upgrade junos-ops   # update

Debian / Ubuntu (.deb)

Pre-built packages for Ubuntu 24.04 (Noble) are available on the GitHub Releases page. The package installs a self-contained Python virtualenv at /opt/venvs/junos-ops/ with junos-ops symlinked to /usr/bin/:

sudo apt install ./junos-ops_*~noble.deb

RHEL / Rocky Linux / AlmaLinux (.rpm)

Pre-built packages for RHEL/Rocky/AlmaLinux 9 are available on the GitHub Releases page. Python 3.12 is required:

sudo dnf install python3.12
sudo rpm -ivh junos-ops-*-1.el9.x86_64.rpm

pip

pip install junos-ops

To upgrade to the latest version:

pip install junos-ops --upgrade

Development Setup

git clone https://github.com/shigechika/junos-ops.git
cd junos-ops
python3 -m venv .venv
. .venv/bin/activate
pip install -e ".[test]"

Dependencies

Tab Completion (optional)

pip install junos-ops[completion]
eval "$(register-python-argcomplete junos-ops)"

Add the eval line to your shell profile (~/.bashrc or ~/.zshrc) to enable it permanently.

Installing pip3 (if not available)

OS-specific instructions
  • Ubuntu/Debian

    sudo apt install python3-pip
    
  • CentOS/RedHat

    sudo dnf install python3-pip
    
  • macOS

    brew install python3
    

Configuration File (config.ini)

An INI-format configuration file that defines connection settings and model-to-package mappings.

The configuration file is searched in the following order (-c / --config can override):

  1. ./config.ini in the current directory
  2. ~/.config/junos-ops/config.ini (XDG_CONFIG_HOME)

Logging

By default junos-ops logs at INFO to the console (stdout, or stderr under --json). -d / --debug raises this to DEBUG; the ncclient / paramiko / jnpr.junos loggers stay at WARNING so -d does not dump every NETCONF/SSH frame.

File logging is opt-in. Give a path with --log-file PATH or log_file = PATH in the [DEFAULT] section of config.ini (the option wins over the config key, ~ is expanded, the parent directory is created on demand). The file rotates daily and keeps 10 backups. Nothing is ever written relative to the current directory unless you ask for it.

For full control (extra handlers, per-logger levels, custom formats) place a logging.ini at one of these locations; it is searched in the same order as config.ini and replaces the defaults above:

  1. ./logging.ini in the current directory
  2. ~/.config/junos-ops/logging.ini (XDG_CONFIG_HOME)

See logging.ini.example for a starting point. It is loaded with disable_existing_loggers=False. -d raises the root logger to DEBUG on top of it, but a level= set on a handler in the file still filters; --log-file / log_file are ignored (with a warning) while a logging.ini is in charge.

DEFAULT Section

Defines global connection settings and model-to-package mappings shared by all hosts.

[DEFAULT]
id = exadmin          # SSH username
pw = password         # SSH password
sshkey = id_ed25519   # SSH private key file
port = 830            # NETCONF port
hashalgo = md5        # Checksum algorithm
rpath = /var/tmp      # Remote path
# ssh_config = ~/.ssh/config   # OpenSSH config (ProxyCommand etc.); if unset, PyEZ auto-picks up ~/.ssh/config
# lpath = ~/firmware   # Local firmware directory (~ expanded, default: current directory)
# log_file = ~/.local/state/junos-ops/junos-ops.log   # Opt-in log file (daily rotation, 10 backups); default: console only
# huge_tree = true    # Allow large XML responses
# RSI_DIR = ./rsi/    # Output directory for RSI/SCF files
# DISPLAY_STYLE = display set   # SCF output style (default: display set)
# DISPLAY_STYLE =               # Empty for stanza format (show configuration only)

# model.file = package filename
# model.hash = checksum value
EX2300-24T.file = junos-arm-32-18.4R3-S10.tgz
EX2300-24T.hash = e233b31a0b9233bc4c56e89954839a8a

The model name must match the model field automatically retrieved from the device.

Host Sections

Each section name becomes the hostname. DEFAULT values can be overridden per host.

[rt1.example.jp]             # Section name is used as the connection hostname
tags = tokyo, core           # Tag-based host filtering (--tags)
[rt2.example.jp]
host = 192.0.2.1             # Override connection target with IP address
tags = osaka, core
[sw1.example.jp]
id = sw1                     # Override SSH username
sshkey = sw1_rsa             # Override SSH key
ssh_config = ~/.ssh/config.lab   # Per-host OpenSSH config (e.g. behind a bastion)
tags = tokyo, access
[sw2.example.jp]
port = 10830                 # Override port
[sw3.example.jp]
EX4300-32F.file = jinstall-ex-4300-20.4R3.8-signed.tgz   # Different version for this host
EX4300-32F.hash = 353a0dbd8ff6a088a593ec246f8de4f4

Usage

junos-ops <subcommand> [options] [hostname ...]

Subcommands

Subcommand Description
upgrade [--unlink] Copy and install package (--unlink for low-flash devices, see below)
copy Copy package from local to remote
install [--unlink] Install a previously copied package (--unlink for low-flash devices, see below)
rollback Rollback to the previous version
version Show running/planning/pending versions and reboot schedule
reboot --at YYMMDDHHMM Schedule a reboot at the specified time
reboot --member N (--now | --at YYMMDDHHMM) [--wait SEC] [--expect-up IFACE,...] Reboot a single Virtual Chassis member (explicit hostnames required; refuses the current Master and mixed-version outcomes unless overridden). --wait verifies the member came back (FPC Online, listed ports up/up)
vc-switch [--wait SEC] hostname … Move Virtual Chassis mastership to the Backup member (request virtual-chassis routing-engine master switch) with fail-closed pre-checks and post-switch verification. Explicit hostnames required. See vc-switch below
snapshot [--force] Create a recovery snapshot (request system snapshot) to sync the alternate boot media; MX-focused. Refuses if the device is running on its alternate media unless --force. See snapshot below
ls [-l] List files on the remote path
show COMMAND [--retry N] / show -f FILE Run an arbitrary CLI command (or file of commands) across devices
check [--connect|--local|--remote|--all] [--model M] Pre-flight checks: NETCONF reachability, local/remote firmware checksum
config -f FILE Push a set command file (see docs/config.md for --confirm, --timeout, --no-confirm, --no-commit, --health-check, --no-health-check)
rsi Collect RSI/SCF in parallel
(none) Show device facts

Common Options

Option Description
hostname Target hostname(s) (defaults to all hosts in config file)
-c, --config CONFIG Config file path (default: config.ini or ~/.config/junos-ops/config.ini)
-n, --dry-run Test run (connect and display messages only, no execution)
-d, --debug Debug output (ncclient/paramiko stay at WARNING; see "Logging")
--log-file PATH Also write INFO logs to PATH (daily rotation, 10 backups). Overrides log_file in config.ini; default: console only
--force Force execution regardless of conditions
--json Emit machine-readable JSON instead of human-readable text. One JSON object per host per line (JSONL); logs are routed to stderr so stdout stays pure JSON. Pipe to jq -s to slurp into an array. See "JSON Output" below.
--tags TAG[,TAG...] Filter hosts by tags. Comma-separates AND together inside one value; repeating --tags ORs groups. Combined with explicit hostnames, the tag filter and hostname list intersect. See "Tag-based Host Filtering" below.
--exclude-tags TAG[,TAG...] Drop hosts whose tags match. Same AND/OR grammar as --tags (comma = AND within a group, repeat to OR groups). Applied after --tags; usable standalone to subtract a subset from the default "all hosts" selection.
--workers N Parallel workers (default: 1 for upgrade, 20 for rsi)
--version Show program version

JSON Output

Pass --json to any subcommand to get machine-readable output. Each host emits one JSON object on its own line (JSON Lines / JSONL), which composes cleanly with parallel --workers N runs:

$ junos-ops version --json rt1.example.jp rt2.example.jp
{"hostname": "rt1.example.jp", "ok": true, "model": "MX240", "running": "22.4R3-S6.5", ...}
{"hostname": "rt2.example.jp", "ok": true, "model": "EX2300-24T", "running": "22.4R3-S6.5", ...}
  • Logs (including config's real-time commit progress) and startup diagnostics (unreadable config, no matching hosts) go to stderr, so 2>/dev/null or piping stdout alone yields valid JSON only. Check the exit code to detect a startup failure (stdout is then empty).
  • A host that fails to connect or errors mid-run still emits a line — {"hostname": ..., "ok": false, "error": ..., "error_message": ...} — so a consumer never silently misses a host.
  • Slurp the stream into a single array with jq -s: junos-ops version --json | jq -s '.'
  • check --json tags each line with "check": "local" (model-keyed inventory rows) or "check": "host" (per-host rows).

Workflow

CLI Architecture Overview

All subcommands share the same execution pipeline: read the config file, determine target hosts (optionally filtered by --tags), then dispatch each host to a worker thread via ThreadPoolExecutor. The --workers N option controls parallelism — defaulting to 1 for upgrade operations (safe sequential execution) and 20 for RSI collection (I/O-bound, benefits from concurrency). Each worker establishes its own NETCONF session, so hosts are processed independently with no shared state.

flowchart TD
    A[junos-ops CLI] --> B[Read config.ini]
    B --> C[Determine target hosts]
    C --> D{Subcommand}
    D --> E[upgrade / copy / install]
    D --> F[version / rollback / reboot]
    D --> G[config / show / ls]
    D --> H[rsi]
    D --> I["(none) → facts"]
    E & F & G & H & I --> J["ThreadPoolExecutor<br/>--workers N"]
    J --> K["NETCONF / SCP<br/>per host"]
    K --> L[Results]

JUNOS Upgrade Workflow

A firmware upgrade follows a four-step sequence designed to minimize risk. First, dry-run verifies connectivity, package availability, and checksum without making changes. Then upgrade copies and installs the package. version confirms the pending version matches expectations before scheduling the reboot. The reboot is scheduled separately so you can choose a maintenance window. If anything goes wrong, rollback reverts to the previous firmware at any point before reboot.

flowchart TD
    A["1. dry-run<br/>junos-ops upgrade -n"] --> B["2. upgrade<br/>junos-ops upgrade"]
    B --> C["3. version<br/>junos-ops version"]
    C --> D["4. reboot<br/>junos-ops reboot --at"]
    D -.->|"if problems"| E["rollback<br/>junos-ops rollback"]
1. Pre-flight check with dry-run
   junos-ops upgrade --dry-run hostname

2. Copy and install with upgrade
   junos-ops upgrade hostname

3. Verify version
   junos-ops version hostname

4. Schedule reboot
   junos-ops reboot --at 2506130500 hostname

Use rollback to revert to the previous version if problems occur.

Upgrade Internal Flow

The upgrade subcommand runs multiple safety checks before and during the update process. It first compares the running version against the target — skipping entirely if already up to date. If a different pending version exists, it rolls that back before proceeding. The copy phase frees disk space (storage cleanup + snapshot delete on EX/QFX), then transfers the package via safe_copy with checksum verification to detect corruption. Before installing, it clears any existing reboot schedule and saves the rescue config as a recovery baseline. Finally, sw.install() validates the package integrity on the device before applying it.

flowchart TD
    A[NETCONF connect] --> B{"Running version<br/>= target?"}
    B -->|yes| C([Skip — already up to date])
    B -->|no| D{"Pending version<br/>exists?"}
    D -->|no| E[copy]
    D -->|yes| F{Pending ≥ target?}
    F -->|yes, no --force| C
    F -->|no / --force| G[Rollback pending version]
    G --> E

    subgraph copy ["copy()"]
        E --> H[Storage cleanup]
        H --> I["Snapshot delete<br/>(EX/QFX only)"]
        I --> J["safe_copy via SCP<br/>+ checksum verification"]:::safe
    end

    J --> K[Clear reboot schedule]
    K --> L[Save rescue config]:::safe
    L --> M["sw.install()<br/>validate + checksum"]:::install
    M --> N([Done — reboot when ready])

    classDef safe fill:#d4edda,stroke:#28a745,color:#000
    classDef install fill:#cce5ff,stroke:#007bff,color:#000

Reboot Safety Flow

Before scheduling a reboot, reboot automatically checks whether the configuration was modified after the firmware install. If changes are detected, it re-saves the rescue config and re-installs with validation to ensure the new firmware is compatible with the current config.

flowchart TD
    A[NETCONF connect] --> B{"Existing reboot<br/>schedule?"}
    B -->|no| D
    B -->|yes| C{--force?}
    C -->|no| B2([Skip — keep existing schedule])
    C -->|yes| CL[Clear existing schedule] --> D

    D{"Pending version<br/>exists?"} -->|no| SCH
    D -->|yes| E[Get last commit time]
    E --> F[Get rescue config time]
    F --> G{"Config modified<br/>after install?"}
    G -->|no| SCH
    G -->|yes| H[Re-save rescue config]:::warned
    H --> I["Re-install firmware<br/>(validate + checksum)"]:::install
    I -->|success| SCH
    I -->|failure| ERR([Abort — do not reboot]):::errstyle

    SCH["Schedule reboot at<br/>--at YYMMDDHHMM"]:::safe

    classDef safe fill:#d4edda,stroke:#28a745,color:#000
    classDef install fill:#cce5ff,stroke:#007bff,color:#000
    classDef warned fill:#fff3cd,stroke:#ffc107,color:#000
    classDef errstyle fill:#f8d7da,stroke:#dc3545,color:#000

Config Push Workflow

The config subcommand uses a safe commit flow: commit confirmed (auto-rollback timer) → health checkcommit (permanent). If the health check fails, the final commit is withheld and JUNOS automatically rolls back when the timer expires.

See docs/config.md for full details including health check options (uptime, ping, CLI commands), commit confirmed flow, --no-confirm, --no-commit, and parallel execution.

1. Preview changes with dry-run
   junos-ops config -f commands.set --dry-run hostname

2. Apply changes
   junos-ops config -f commands.set hostname

3. Apply with NETCONF health check (no ping dependency)
   junos-ops config -f commands.set --health-check uptime hostname

Tag-based Host Filtering

Use --tags to target hosts by tags defined in config.ini. The flag combines two operators so you can build almost any selection:

  • AND within a group: comma-separated tags in one --tags value require the host to carry all of them (--tags tokyo,core → hosts tagged both tokyo AND core).
  • OR between groups: repeat --tags to add another group. A host matches if it satisfies any group (--tags main --tags backup → hosts tagged either main or backup).
  • AND with a hostname list: when explicit hostnames are also given, the tag filter and the hostname list intersect — only hostnames that also satisfy the tag filter are targeted.

In logical terms: (--tags group₁) OR (--tags group₂) OR … filters the host set, and [hostname …] further restricts that set to the listed names.

History: v0.16.3 and earlier treated --tags + hostnames as union. v0.16.4 switched to intersection so --tags reads as a safety filter ("narrow further by name"). v0.16.6 added repeatable --tags for OR between groups — previously the last --tags occurrence silently replaced earlier ones.

# All hosts tagged "tokyo"
junos-ops version --tags tokyo

# Hosts tagged both "tokyo" AND "core"
junos-ops version --tags tokyo,core

# Hosts tagged "main" OR "backup"
junos-ops check --remote --tags main --tags backup

# (tokyo AND core) OR access — any host that is a tokyo core router
# or carries the access tag.
junos-ops version --tags tokyo,core --tags access

# Among "backup"-tagged hosts, target only these two (tag filter +
# hostname list = intersection)
junos-ops copy --tags backup rt1.example.jp rt2.example.jp

Excluding hosts with --exclude-tags

--exclude-tags removes hosts from the selection using the same grammar as --tags. It runs after --tags and the hostname intersection, so it acts as a final "drop these" pass.

  • --exclude-tags a,b drops hosts tagged with both a and b (AND within a group).
  • Repeating the flag ORs groups, so --exclude-tags a --exclude-tags b drops hosts tagged with a or b.
  • --exclude-tags may be used on its own; without --tags or a hostname list it subtracts from the default "all sections" set.
  • If every candidate host is dropped, junos-ops exits with an error instead of silently no-op'ing.
# All hosts tagged "main" except SRX345 boxes
junos-ops check --remote --tags main --exclude-tags srx345

# Run against every host except the lab tier
junos-ops version --exclude-tags lab

# Drop SRX345 OR EOL hosts from the main set
junos-ops check --remote --tags main \
                --exclude-tags srx345 --exclude-tags eol

Examples

upgrade (package update)

% junos-ops upgrade rt1.example.jp
# rt1.example.jp
remote: jinstall-ppc-18.4R3-S10-signed.tgz is not found.
copy: system storage cleanup successful
rt1.example.jp: cleaning filesystem ...
rt1.example.jp: b'jinstall-ppc-18.4R3-S10-signed.tgz': 380102074 / 380102074 (100%)
rt1.example.jp: checksum check passed.
install: clear reboot schedule successful
install: rescue config save successful
rt1.example.jp: software validate package-result: 0

version (version check)

% junos-ops version rt1.example.jp
# rt1.example.jp
  - hostname: rt1
  - model: MX5-T
  - running version: 18.4R3-S7.2
  - planning version: 18.4R3-S10
    - running='18.4R3-S7.2' < planning='18.4R3-S10'
  - pending version: 18.4R3-S10
    - running='18.4R3-S7.2' < pending='18.4R3-S10' : Please plan to reboot.
  - reboot requested by exadmin at Sat Dec  4 05:00:00 2021

check (pre-flight verification)

Unified pre-flight checks across NETCONF reachability and firmware checksums. Exit code is non-zero if any check fails. Default (no flag) runs --connect only.

--local is inventory-mode and ignores hostnames — it iterates every <model>.file / <model>.hash pair in config.ini and verifies the files on the staging server. No NETCONF required:

% junos-ops check --local
lpath: /opt/firmware
model            file                                                        checksum
---------------  ----------------------------------------------------------  ----------
ex2300-24t       junos-arm-32-23.4R2-S7.4.tgz                                ok
ex3400-24t       junos-arm-32-23.4R2-S7.4.tgz                                ok(cached)
ex4300-32f       jinstall-ex-4300-21.4R3-S12.2-signed.tgz                    ok
mx5-t            jinstall-ppc-21.2R3-S8.5-signed.tgz                         missing

  mx5-t: - local package: /opt/firmware/jinstall-ppc-21.2R3-S8.5-signed.tgz is not found.

Use --model M to restrict the inventory to a single model. For multi-model selection, attach <model>.tags = ... to the relevant rows in config.ini and use --tags (and optionally --exclude-tags) — the same AND/OR grammar as host-side --tags applies, but selectors are matched against the model name and the model's <model>.tags set, not against host tags:

[DEFAULT]
ex2300-24t.file = junos-arm-32-23.4R2-S7.4.tgz
ex2300-24t.hash = ...
ex2300-24t.tags = main, edge       # optional model tags
mx240.file       = ...
mx240.tags       = backbone
# Single model — matches by name, no <model>.tags needed
% junos-ops check --local --tags ex2300-24t

# "main"-tagged models — uses <model>.tags
% junos-ops check --local --tags main

# Multi-selection: model name + tag groups OR together
% junos-ops check --local --tags srx345 --tags backbone

# Drop a model from the result
% junos-ops check --local --tags main --exclude-tags ex3400-24t

For --local-only runs, host-side --tags (the per-host tag selector) is not applied — --local is host-independent, so --tags reinterprets as the model selector described above. Mixing --local with --connect / --remote keeps the host selector active for the per-host part.

Note on --all (or --local --connect/--remote): a single --tags value filters both tables at once — the inventory table by <model>.tags (and model name) and the per-host table by host tags. The two tag namespaces are independent, so keep their names aligned (e.g. a main model tag and a main host tag) if you want --all --tags main to scope both consistently.

--connect / --remote (and --all) are per-host and use the specified hostnames (or every host in config.ini, optionally filtered by --tags). --remote doubles as a "did the SCP copy fully land" check between copy and install:

% junos-ops check --connect --remote rt1.example.jp rt2.example.jp
hostname         connect  remote      model     file
---------------  -------  ----------  --------  -----------------------------------
rt1.example.jp   ok       ok          MX5-T     jinstall-ppc-18.4R3-S10-signed.tgz
rt2.example.jp   ok       missing     MX5-T     jinstall-ppc-18.4R3-S10-signed.tgz

  rt2.example.jp: remote: - remote package: jinstall-ppc-18.4R3-S10-signed.tgz is not found.

When --connect / --remote need to resolve a model and it was not supplied via --model, they fall back to dev.facts["model"] from the live device, or to an optional model = MX5-T key added to the host section in config.ini.

--all runs both: the inventory table is printed first, then the per-host table.

rsi (parallel RSI/SCF collection)

% junos-ops rsi --workers 5 rt1.example.jp rt2.example.jp
# rt1.example.jp
  rt1.example.jp.SCF done
  rt1.example.jp.RSI done
# rt2.example.jp
  rt2.example.jp.SCF done
  rt2.example.jp.RSI done

The output directory comes from RSI_DIR in config.ini, but --rsi-dir DIR overrides it per run (default: current directory). ~ is expanded and the directory is created if it does not exist.

reboot (scheduled reboot)

% junos-ops reboot --at 2506130500 rt1.example.jp
# rt1.example.jp
	Shutdown at Fri Jun 13 05:00:00 2025. [pid 97978]

Rebooting a single Virtual Chassis member

--member N issues request system reboot member N instead of rebooting the whole chassis, and --now (only together with --member) reboots immediately instead of at a scheduled time. Explicit hostnames are required — the implicit "all hosts in config.ini" target never applies to a member reboot.

% junos-ops reboot --member 0 --now sw1.example.jp
# sw1.example.jp
reboot member 0 now
	member 0: role=Backup status=Prsnt (master=1, backup=0)
	Shutdown NOW!

Before issuing anything the member is checked against show virtual-chassis status:

  • the member must exist and be Prsnt;
  • rebooting the current Master is refused — switch mastership away first (see vc-switch) or pass --force if you really mean it;
  • if a package is installed but not yet booted (pending), a single-member reboot would activate it on that member only and leave the VC mixed-version; this is refused unless --allow-mixed-version.

--wait SEC (only with --member --now) verifies the recovery instead of leaving it to you: junos-ops reads the member's boot timestamp before issuing the reboot, then reconnects until that timestamp has changed and the member is Prsnt with a role and its FPC slot is Online. (The reboot RPC returns while the member is still up, so "healthy" on its own is not evidence that anything happened; if the boot time cannot be read, junos-ops instead requires having seen the member go away, and says so.) Add --expect-up ge-0/0/40,xe-0/0/47 (physical or logical names) to also require those interfaces to be up/up — a member can be Prsnt while its PFE is still not forwarding, which is exactly when traffic hashed to it is black-holed. Connection failures during the window are "not yet": the whole VC can be unreachable while one member reboots if the management path transits its uplink. Exit code is 10 when the member is not back in time, and the JSON output carries wait, after, fpc_state and interfaces.

% junos-ops reboot --member 0 --now --wait 600 --expect-up xe-0/0/47 sw1.example.jp
# sw1.example.jp
reboot member 0 now
	member 0: role=Backup status=Prsnt (master=1, backup=0)
	Rebooting fpc0
	confirmed: member 0 is back (FPC Online, 1 port(s) up) after 372s / 14 probe(s)

The existing-schedule check and --force schedule clearing look at that member's own fpcN: block of show system reboot (and clear with clear system reboot member N); the config-drift/reinstall gate applies as for a whole-chassis reboot. If the pending-package check itself fails the reboot is refused rather than assumed clean. --member also works with --at to schedule a member reboot.

vc-switch (move Virtual Chassis mastership)

% junos-ops vc-switch sw1.example.jp
# sw1.example.jp
vc-switch: confirmed
  expected master after switch: member 1
before:
  member 0: Master   Prsnt
  member 1: Backup   Prsnt
after:
  member 0: Backup   Prsnt
  member 1: Master   Prsnt
	virtual-chassis: member 0=Master/Prsnt, member 1=Backup/Prsnt
	task replication: GRES=Enabled RE=Master OSPF=Complete, OSPF3=Complete
	'request virtual-chassis routing-engine master switch' issued; session dropped (RpcTimeoutError) — expected during a mastership switch
	confirmed: member 1 is Master after 3 probe(s)

Runs the mastership switch once, wrapped in the checks a bare junos-ops show "request …" cannot give you. The EX Virtual Chassis form (request virtual-chassis routing-engine master switch) is tried first; if the device raises a parse/platform rejection — QFX VCs answer "command is not valid on the qfx5110-…" — the chassis form (request chassis routing-engine master switch no-confirm) is sent instead. Each form is issued at most once. Anything that comes back as text ends the attempt, whatever it says: the CLI processed the command, so a second destructive form is never sent.

  • Pre-checks (fail closed): show virtual-chassis status must report exactly one Master and one Backup with every member Prsnt; show task replication must report GRES Enabled, RE mode Master, and every listed protocol Complete (nothing listed is treated as not safe — without NSR the switch drops routing adjacencies). If either RPC fails the switch is refused — "could not check" is never "probably fine". --force turns refusals into warnings and proceeds.
  • One shot: the command is never retried. The NETCONF session usually dies with the switch (RpcTimeoutError / connection closed); that is recorded as "issued, session dropped", not as a failure. Only an RpcError or an explicit rejection in the reply counts as failure.
  • Verification (--wait SEC, default 180): reconnects until the former Backup reports itself as Master, then re-reads show task replication (reported as a warning if not yet Complete — it takes minutes and is not a gate). --wait 0 issues and returns without verifying; the output then says so explicitly.
  • -n / --dry-run runs the pre-checks and prints what would be issued. Explicit hostnames are required; the implicit all-hosts target never applies.

A rejection is also checked against the device: when the command was issued but the reply read as a refusal, --wait still looks at show virtual-chassis status. If mastership moved anyway the result is upgraded to confirmed, keeping the device's wording in rejected_reply and warning that this command may not be what moved it (a concurrent or manual switch looks identical); if it did not, the rejection stands, now backed by the device state rather than by its wording.

Exit code 0 only for confirmed, dry_run and (with --wait 0) initiated_unverified; refused, rejected and verification_failed return 1. With --json gate the next step on status == "confirmed" (or verified), never on ok alone:

junos-ops vc-switch --json sw1.example.jp | jq -e '.status == "confirmed"' \
  && junos-ops config -f drain-member0.set --confirm 5 sw1.example.jp \
  && junos-ops reboot --member 0 --now sw1.example.jp

snapshot (sync the alternate boot media)

% junos-ops snapshot rt1.example.jp
# rt1.example.jp
	snapshot: 'request system snapshot' completed

Runs request system snapshot to copy the running system (root + config) onto the alternate (backup) boot media.

A JUNOS upgrade only rewrites the currently-running media; the alternate is not refreshed automatically and drifts over time ("fossil alternate"). If the primary later fails to boot, the device falls back to that stale alternate and may come up non-routable. Taking a snapshot — before a risky change, or periodically — keeps the alternate current so the fallback is safe.

  • MX is the primary target (both fixed-config MX5/MX80 dual-eUSB and RE/disk-based MX240/480), where this problem actually bites.
  • EX/QFX (SWITCH) are supported, but EX2300/EX3400 are chronically tight on free disk and the recovery snapshot may not fit; an out-of-space failure is reported as a clean, non-fatal skip rather than an error.
  • Branch SRX (SRX300/320/340/345/380) are supported but low priority — they keep the alternate slice in sync during normal upgrades, so an explicit snapshot is usually unnecessary (uses request system snapshot slice alternate).
  • Other platforms (vmhost MX such as MX204/MX10003, mid/high-end SRX, HA clusters, vMX/vSRX, Junos Evolved, unknown) are skipped (non-fatal) — no command is issued on hardware whose snapshot behaviour is not verified.

Safety guard: snapshot refuses to run on a device that appears to be running on its alternate media (which would clone a possibly-stale system onto the primary). Override with --force only when you are sure the running image is the one you want to propagate. The on-device detection of the alternate-media state is best-effort and may report "inconclusive" (the snapshot then proceeds with a warning).

config (push set command file)

Push a set-format command file to multiple devices. Uses a safe commit flow: commit check, commit confirmed, then confirm.

% cat add-user.set
set system login user viewer class read-only
set system login user viewer authentication ssh-ed25519 "ssh-ed25519 AAAA..."

% junos-ops config -f add-user.set --dry-run rt1.example.jp rt2.example.jp
# rt1.example.jp
[edit system login]
+    user viewer {
+        class read-only;
+        authentication {
+            ssh-ed25519 "ssh-ed25519 AAAA...";
+        }
+    }
	dry-run: rollback (no commit)
# rt2.example.jp
	...

% junos-ops config -f add-user.set rt1.example.jp rt2.example.jp
# rt1.example.jp
	...
	commit check passed
	commit confirmed 1 applied
	health check: uptime (NETCONF RPC)
	health check passed (uptime: 2026-04-16 13:20:31 JST)
	commit confirmed, changes are now permanent
# rt2.example.jp
	...

Use --confirm N to change the commit confirmed timeout (default: 1 minute). Use --no-health-check to skip the post-commit health check.

Set files can include # comments and blank lines — they are automatically stripped before sending to the device.

Jinja2 Templates

Use .j2 files to generate per-host configurations from a single template. Variables come from var_* keys in config.ini and device facts.

junos-ops config -f ntp.set.j2 --dry-run rt1.example.jp sw1.example.jp
junos-ops config -f ntp.set.j2 rt1.example.jp sw1.example.jp

See docs/template.md for details, usage patterns (conditionals, loops), and config.ini examples.

show (run CLI command)

Run an arbitrary CLI command across multiple devices in parallel. Use --retry N to automatically retry on RpcTimeoutError (useful for large-scale parallel execution).

% junos-ops show "show bgp summary" --config accounts.ini gw1.example.jp gw2.example.jp
# gw1.example.jp
Groups: 4 Peers: 6 Down peers: 0
...
# gw2.example.jp
Groups: 3 Peers: 4 Down peers: 0
...

Use -f to run multiple commands from a file within a single NETCONF session per device:

% cat commands.txt
# security policy check
show security policies hit-count
show security flow session summary

% junos-ops show -f commands.txt --config accounts.ini fw1.example.jp
# fw1.example.jp
## show security policies hit-count
...

## show security flow session summary
...

Use --retry N to retry commands that fail with RpcTimeoutError (with incremental backoff: 5s, 10s, 15s, ...):

% junos-ops show "show system alarms" --retry 2 --workers 10 --config accounts.ini

Structured output (--format)

Use -F / --format {text,json,xml} to change the output format. text is the default (legacy behaviour); json is handy for programmatic consumers and AI assistants; xml returns the pretty-printed RPC reply.

% junos-ops show "show interfaces terse" --format json gw1.example.jp
# gw1.example.jp
## show interfaces terse
{
  "interface-information": {
    "physical-interface": [ ... ]
  }
}

Caveat — pipe stages are ignored by the device regardless of --format. dev.cli() sends the command over NETCONF RPC, and the device does not process pipe stages for that path — modifiers like | match, | last, and | count are silently ignored no matter which format is requested, text included (see Juniper/junos-mcp-server#4 / #12, whose own NETCONF trace shows a format="text" request still losing its pipe stage). The one exception is | display xml rpc, which the device honours specially to return the equivalent RPC method name instead of normal output. If you need to filter output, pipe it through shell tools client-side (e.g. grep) or call the equivalent RPC directly. For json, note that -F json alone still wraps output in the # hostname / ## command headers shown above — combine it with the top-level --json flag (junos-ops --json show ... -F json host) to get clean, header-free JSON on stdout before piping to jq.

No subcommand (show device facts)

% junos-ops gw1.example.jp
# gw1.example.jp
{'2RE': True,
 'hostname': 'gw1',
 'model': 'MX240',
 'version': '18.4R3-S7.2',
 ...}

Upgrading low-flash devices (--unlink)

On low-flash devices like EX2300/EX3400 (/dev/gpt/junos = 1.3GB), major upgrades such as 22.4 → 23.4 may fail at the validation step with ERROR: insufficient space. This is because PyEZ's SW.install() does not expose the unlink option of request system software add — its default path never asks pkgadd to unlink the source tgz, so the original package occupies space throughout extraction.

Pass --unlink to invoke request system software add <package> unlink directly via the CLI, so pkgadd unlinks the tgz during installation and frees space as it extracts.

# For low-flash devices
junos-ops upgrade --unlink ex3400-host.example.jp

# Same flag is available on the install-only subcommand
junos-ops install --unlink ex3400-host.example.jp

Supported Models

Any Juniper model can be supported by defining the model name and package file in the configuration file. Models included in the example configuration:

Series Example Models
EX EX2300-24T, EX3400-24T, EX4300-32F
MX MX5-T, MX240
QFX QFX5110-48S-4C
SRX SRX300, SRX345, SRX1500, SRX4600

License

Apache License 2.0

Copyright 2022-2025 AIKAWA Shigechika

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

junos_ops-0.30.0.tar.gz (182.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

junos_ops-0.30.0-py3-none-any.whl (96.2 kB view details)

Uploaded Python 3

File details

Details for the file junos_ops-0.30.0.tar.gz.

File metadata

  • Download URL: junos_ops-0.30.0.tar.gz
  • Upload date:
  • Size: 182.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for junos_ops-0.30.0.tar.gz
Algorithm Hash digest
SHA256 ab85934727ecee66fa2bbb7e72f064d90b14fd386fd85e1505082c4d46d93908
MD5 eeff0dbaa17e0923d21bbccb0e771210
BLAKE2b-256 2000381484fa105fafd3040684c93a4414df2afb5b8516f07be5ccd5ab180251

See more details on using hashes here.

Provenance

The following attestation bundles were made for junos_ops-0.30.0.tar.gz:

Publisher: release.yml on shigechika/junos-ops

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file junos_ops-0.30.0-py3-none-any.whl.

File metadata

  • Download URL: junos_ops-0.30.0-py3-none-any.whl
  • Upload date:
  • Size: 96.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for junos_ops-0.30.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa27c1ff93973752dfb7160a4b6fe6d94637e7cb91f85d73e9749d319a790fde
MD5 43b0304046ab0809139a0cffc6b617cf
BLAKE2b-256 ddc2811eb40545a7c86de1e28e7b60527aaffa72aab026b142c32344d5154465

See more details on using hashes here.

Provenance

The following attestation bundles were made for junos_ops-0.30.0-py3-none-any.whl:

Publisher: release.yml on shigechika/junos-ops

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.30.0 This release

2 files

0.29.2

2 files

0.29.1

2 files

0.29.0

2 files

0.28.0

2 files

0.27.2

2 files

0.27.1

2 files

0.27.0

2 files

0.26.2

2 files

0.26.1

2 files

0.26.0

2 files

0.25.1

2 files

0.25.0

2 files

0.24.0

2 files

0.23.1

2 files

0.23.0

2 files

0.22.2

2 files

0.22.1

2 files

0.22.0

2 files

0.21.2

2 files

0.21.1

2 files

0.21.0

2 files

0.20.0

2 files

0.19.0

2 files

0.18.5

2 files

0.18.4

2 files

0.18.3

2 files

0.18.2

2 files

0.18.1

2 files

0.18.0

2 files

0.17.0

2 files

0.16.10

2 files

0.16.9

2 files

0.16.8

2 files

0.16.7

2 files

0.16.6

2 files

0.16.5

2 files

0.16.4

2 files

0.16.3

2 files

0.16.2

2 files

0.16.1

2 files

0.16.0

2 files

0.15.0

2 files

0.14.1

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.2

2 files

0.11.1

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page