Metadata-Version: 2.4
Name: junos-ops
Version: 0.26.1
Summary: Automated JUNOS package management tool for Juniper Networks devices
Author: AIKAWA Shigechika
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/shigechika/junos-ops
Project-URL: Repository, https://github.com/shigechika/junos-ops
Project-URL: Issues, https://github.com/shigechika/junos-ops/issues
Project-URL: Changelog, https://github.com/shigechika/junos-ops/blob/main/CHANGELOG.md
Keywords: juniper,junos,netconf,pyez,network,automation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: junos-eznc
Requires-Dist: looseversion
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Provides-Extra: completion
Requires-Dist: argcomplete; extra == "completion"
Provides-Extra: template
Requires-Dist: Jinja2>=3.1; extra == "template"
Provides-Extra: progress
Requires-Dist: tqdm>=4; extra == "progress"
Dynamic: license-file

# junos-ops

[![PyPI version](https://img.shields.io/pypi/v/junos-ops)](https://pypi.org/project/junos-ops/)
[![CI](https://github.com/shigechika/junos-ops/actions/workflows/ci.yml/badge.svg)](https://github.com/shigechika/junos-ops/actions/workflows/ci.yml)
[![Python](https://img.shields.io/pypi/pyversions/junos-ops)](https://pypi.org/project/junos-ops/)

[日本語版 / Japanese](https://github.com/shigechika/junos-ops/blob/main/README.ja.md)

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](docs/template.md))
- 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](#installation)
- [Configuration File (config.ini)](#configuration-file-configini)
- [Usage](#usage)
- [Workflow](#workflow)
- [Examples](#examples)
- [Supported Models](#supported-models)
- [License](#license)

## Installation

### Homebrew (macOS)

```bash
brew install shigechika/tap/junos-ops
```

### Debian / Ubuntu (.deb)

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

```bash
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](https://github.com/shigechika/junos-ops/releases) page. Python 3.12 is required:

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

### pip

```bash
pip install junos-ops
```

To upgrade to the latest version:

```bash
pip install junos-ops --upgrade
```

### Development Setup

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

### Dependencies

- [junos-eznc (PyEZ)](https://www.juniper.net/documentation/product/us/en/junos-pyez) — Juniper NETCONF automation library
- [looseversion](https://pypi.org/project/looseversion/) — Version comparison

### Tab Completion (optional)

```bash
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)

<details>
<summary>OS-specific instructions</summary>

- **Ubuntu/Debian**
  ```bash
  sudo apt install python3-pip
  ```

- **CentOS/RedHat**
  ```bash
  sudo dnf install python3-pip
  ```

- **macOS**
  ```bash
  brew install python3
  ```

</details>

## 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 Configuration (logging.ini)

An optional `logging.ini` file can be used to customize log output (e.g., suppress verbose paramiko/ncclient messages). The file is searched in the same order as `config.ini`:

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

If neither is found, the default logging configuration (INFO level to stdout) is used.

### DEFAULT Section

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

```ini
[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)
# 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.

```ini
[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 |
| `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](#snapshot-sync-the-alternate-boot-media) 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](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 |
| `--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:

```console
$ 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.

```mermaid
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.

```mermaid
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.

```mermaid
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.

```mermaid
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 check** → `commit` (permanent). If the health check fails, the final `commit` is withheld and JUNOS automatically rolls back when the timer expires.

See [docs/config.md](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:

```ini
[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).

### 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]
```

### 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.

```bash
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](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](https://github.com/Juniper/junos-mcp-server/issues/4) / [#12](https://github.com/Juniper/junos-mcp-server/issues/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.

```bash
# 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](LICENSE)

Copyright 2022-2025 AIKAWA Shigechika
