feat: Add gNMI discovery CLI tools for YANG path exploration #21
32
README.md
32
README.md
@@ -69,12 +69,12 @@ Reference: [arista-evpn-vxlan-clab](https://gitea.arnodo.fr/Damien/arista-evpn-v
|
|||||||
|
|
||||||
Progress is tracked via issues. See [all issues](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues) or filter by phase:
|
Progress is tracked via issues. See [all issues](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues) or filter by phase:
|
||||||
|
|
||||||
| Phase | Description | Issues |
|
| Phase | Description | Issues |
|
||||||
|-------|-------------|--------|
|
| ----------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Phase 1** | YANG Path Discovery - Map EOS 4.35.0F YANG models, validate gNMI | [phase-1-yang-discovery](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=1) |
|
| **Phase 1** | YANG Path Discovery - Map EOS 4.35.0F YANG models, validate gNMI | [phase-1-yang-discovery](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=1) |
|
||||||
| **Phase 2** | Minimal Reconciler - VLANs/VNIs, diff engine, CLI plan/apply | [phase-2-minimal-reconciler](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=2) |
|
| **Phase 2** | Minimal Reconciler - VLANs/VNIs, diff engine, CLI plan/apply | [phase-2-minimal-reconciler](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=2) |
|
||||||
| **Phase 3** | Full Fabric - BGP, MLAG, VRFs, dependency ordering | [phase-3-full-fabric](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=3) |
|
| **Phase 3** | Full Fabric - BGP, MLAG, VRFs, dependency ordering | [phase-3-full-fabric](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=3) |
|
||||||
| **Phase 4** | Event-Driven - gNMI Subscribe, drift detection, webhooks | [phase-4-event-driven](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=4) |
|
| **Phase 4** | Event-Driven - gNMI Subscribe, drift detection, webhooks | [phase-4-event-driven](https://gitea.arnodo.fr/Damien/fabric-orchestrator/issues?type=all&state=all&labels=4) |
|
||||||
|
|
||||||
📌 **Project Board**: [View Kanban](https://gitea.arnodo.fr/Damien/fabric-orchestrator/projects)
|
📌 **Project Board**: [View Kanban](https://gitea.arnodo.fr/Damien/fabric-orchestrator/projects)
|
||||||
|
|
||||||
@@ -115,16 +115,16 @@ fabric-orchestrator/
|
|||||||
|
|
||||||
## 🛠️ Technology Stack
|
## 🛠️ Technology Stack
|
||||||
|
|
||||||
| Component | Technology | Purpose |
|
| Component | Technology | Purpose |
|
||||||
|-----------|------------|---------|
|
| --------------- | -------------------------- | ------------------------------------ |
|
||||||
| Source of Truth | NetBox | Intent definition via ConfigContexts |
|
| Source of Truth | NetBox | Intent definition via ConfigContexts |
|
||||||
| Transport | gNMI | Configuration and telemetry |
|
| Transport | gNMI | Configuration and telemetry |
|
||||||
| Data Models | YANG (OpenConfig + Arista) | Structured configuration |
|
| Data Models | YANG (OpenConfig + Arista) | Structured configuration |
|
||||||
| Orchestrator | Python (asyncio) | Reconciliation engine |
|
| Orchestrator | Python (asyncio) | Reconciliation engine |
|
||||||
| CLI | Click + Rich | User interface |
|
| CLI | Click + Rich | User interface |
|
||||||
| API | FastAPI | Webhook receiver |
|
| API | FastAPI | Webhook receiver |
|
||||||
| Event Bus | Redis | Async event handling |
|
| Event Bus | Redis | Async event handling |
|
||||||
| Lab | ContainerLab + cEOS | Development environment |
|
| Lab | ContainerLab + cEOS | Development environment |
|
||||||
|
|
||||||
## 🔗 Related Projects
|
## 🔗 Related Projects
|
||||||
|
|
||||||
|
|||||||
626
docs/cli-user-guide.md
Normal file
626
docs/cli-user-guide.md
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
# Fabric Orchestrator CLI User Guide
|
||||||
|
|
||||||
|
This guide covers the `fabric-orch` command-line interface for exploring YANG paths and managing Arista EVPN-VXLAN fabrics via gNMI.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [Commands Overview](#commands-overview)
|
||||||
|
- [discover capabilities](#discover-capabilities)
|
||||||
|
- [discover get](#discover-get)
|
||||||
|
- [discover set](#discover-set)
|
||||||
|
- [discover subscribe](#discover-subscribe)
|
||||||
|
- [discover paths](#discover-paths)
|
||||||
|
- [Output Formats](#output-formats)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Best Practices](#best-practices)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Ensure you have Python 3.12+ and `uv` installed, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://gitea.arnodo.fr/Damien/fabric-orchestrator.git
|
||||||
|
cd fabric-orchestrator
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
uv run fabric-orch --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables (optional but recommended)
|
||||||
|
export GNMI_TARGET="172.16.0.50:6030"
|
||||||
|
export GNMI_USERNAME="admin"
|
||||||
|
export GNMI_PASSWORD="admin"
|
||||||
|
|
||||||
|
# List device capabilities
|
||||||
|
uv run fabric-orch discover capabilities
|
||||||
|
|
||||||
|
# Get interface state
|
||||||
|
uv run fabric-orch discover get --path "/interfaces/interface[name=Ethernet1]/state"
|
||||||
|
|
||||||
|
# Show common YANG paths reference
|
||||||
|
uv run fabric-orch discover paths
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The CLI supports these environment variables to avoid passing credentials repeatedly:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| --------------- | ----------------------------------- | ---------- |
|
||||||
|
| `GNMI_TARGET` | Target device in `host:port` format | (required) |
|
||||||
|
| `GNMI_USERNAME` | Username for gNMI authentication | `admin` |
|
||||||
|
| `GNMI_PASSWORD` | Password for gNMI authentication | `admin` |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
export GNMI_TARGET="leaf1:6030"
|
||||||
|
export GNMI_USERNAME="admin"
|
||||||
|
export GNMI_PASSWORD="admin"
|
||||||
|
|
||||||
|
# Now you can omit --target, --username, --password
|
||||||
|
uv run fabric-orch discover capabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
fabric-orch
|
||||||
|
└── discover # YANG path discovery tools
|
||||||
|
├── capabilities # List device capabilities
|
||||||
|
├── get # Get config/state data
|
||||||
|
├── set # Set configuration
|
||||||
|
├── subscribe # Subscribe to updates
|
||||||
|
└── paths # Show common paths reference
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## discover capabilities
|
||||||
|
|
||||||
|
List device capabilities and supported YANG models.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fabric-orch discover capabilities [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
| -------------------------- | ----- | -------------------------------------- |
|
||||||
|
| `--target` | `-t` | Target device (host:port) |
|
||||||
|
| `--username` | `-u` | Username for authentication |
|
||||||
|
| `--password` | `-p` | Password for authentication |
|
||||||
|
| `--insecure/--no-insecure` | | Skip TLS verification (default: True) |
|
||||||
|
| `--output` | `-o` | Output format: pretty, json, file:path |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage with all options
|
||||||
|
uv run fabric-orch discover capabilities \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--username admin \
|
||||||
|
--password admin \
|
||||||
|
--insecure
|
||||||
|
|
||||||
|
# Using environment variables
|
||||||
|
export GNMI_TARGET="172.16.0.50:6030"
|
||||||
|
uv run fabric-orch discover capabilities
|
||||||
|
|
||||||
|
# Output as JSON
|
||||||
|
uv run fabric-orch discover capabilities --output json
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
uv run fabric-orch discover capabilities --output file:capabilities.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Device Capabilities - 172.16.0.50:6030
|
||||||
|
|
||||||
|
gNMI Version: 0.8.0
|
||||||
|
Supported Encodings: json_ietf, json, proto
|
||||||
|
|
||||||
|
Supported Models (156):
|
||||||
|
|
||||||
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
||||||
|
┃ Model Name ┃ Organization ┃ Version ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
|
||||||
|
│ arista-exp-eos-evpn │ Arista Networks │ 2023-01-01 │
|
||||||
|
│ arista-exp-eos-mlag │ Arista Networks │ 2023-01-01 │
|
||||||
|
│ arista-exp-eos-vxlan │ Arista Networks │ 2023-01-01 │
|
||||||
|
│ openconfig-interfaces │ OpenConfig │ 2.4.3 │
|
||||||
|
│ openconfig-network-instance │ OpenConfig │ 1.1.0 │
|
||||||
|
│ ... │ ... │ ... │
|
||||||
|
└───────────────────────────────────┴─────────────────┴────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## discover get
|
||||||
|
|
||||||
|
Get configuration or state data at a YANG path.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fabric-orch discover get [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
| -------------------------- | ----- | -------------------------------------------- |
|
||||||
|
| `--target` | `-t` | Target device (host:port) |
|
||||||
|
| `--username` | `-u` | Username for authentication |
|
||||||
|
| `--password` | `-p` | Password for authentication |
|
||||||
|
| `--insecure/--no-insecure` | | Skip TLS verification |
|
||||||
|
| `--output` | `-o` | Output format: pretty, json, file:path |
|
||||||
|
| `--path` | `-P` | YANG path to get data from **(required)** |
|
||||||
|
| `--type` | `-T` | Data type: config, state, all (default: all) |
|
||||||
|
| `--depth` | `-d` | Maximum depth (not implemented yet) |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all data at a path
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]"
|
||||||
|
|
||||||
|
# Get config only
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]" \
|
||||||
|
--type config
|
||||||
|
|
||||||
|
# Get state only
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/state" \
|
||||||
|
--type state
|
||||||
|
|
||||||
|
# Get BGP neighbor state
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state"
|
||||||
|
|
||||||
|
# Get VXLAN VNI mappings
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis"
|
||||||
|
|
||||||
|
# Save output to file
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface" \
|
||||||
|
--output file:interfaces.json
|
||||||
|
|
||||||
|
# Machine-readable JSON output
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/system/config" \
|
||||||
|
--output json | jq '.notification[0].update[0].val'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Syntax
|
||||||
|
|
||||||
|
YANG paths use a specific syntax:
|
||||||
|
|
||||||
|
- **Basic path**: `/interfaces/interface`
|
||||||
|
- **With key**: `/interfaces/interface[name=Ethernet1]`
|
||||||
|
- **Multiple keys**: `/protocols/protocol[identifier=BGP][name=BGP]`
|
||||||
|
- **Nested path**: `/interfaces/interface[name=Ethernet1]/state/oper-status`
|
||||||
|
|
||||||
|
### Common Paths
|
||||||
|
|
||||||
|
| Category | Path | Description |
|
||||||
|
| ---------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------- |
|
||||||
|
| Interfaces | `/interfaces/interface[name=Ethernet1]/state` | Interface state |
|
||||||
|
| BGP | `/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state` | BGP neighbors |
|
||||||
|
| VXLAN | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis` | VNI mappings |
|
||||||
|
| MLAG | `/arista/eos/mlag/config` | MLAG config |
|
||||||
|
| System | `/system/config/hostname` | Hostname |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## discover set
|
||||||
|
|
||||||
|
Set configuration at a YANG path.
|
||||||
|
|
||||||
|
> ⚠️ **Warning**: This command modifies device configuration. Use with caution!
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fabric-orch discover set [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
| -------------------------- | ----- | ---------------------------------------------------- |
|
||||||
|
| `--target` | `-t` | Target device (host:port) |
|
||||||
|
| `--username` | `-u` | Username for authentication |
|
||||||
|
| `--password` | `-p` | Password for authentication |
|
||||||
|
| `--insecure/--no-insecure` | | Skip TLS verification |
|
||||||
|
| `--output` | `-o` | Output format: pretty, json, file:path |
|
||||||
|
| `--path` | `-P` | YANG path to set **(required)** |
|
||||||
|
| `--value` | `-v` | Value to set (JSON or string) **(required)** |
|
||||||
|
| `--operation` | `-O` | Operation: update, replace, delete (default: update) |
|
||||||
|
| `--dry-run/--no-dry-run` | | Dry-run mode (default: True) |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry-run (default) - shows what would be set
|
||||||
|
uv run fabric-orch discover set \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config/description" \
|
||||||
|
--value "Uplink to Spine1"
|
||||||
|
|
||||||
|
# Actually apply the change
|
||||||
|
uv run fabric-orch discover set \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config/description" \
|
||||||
|
--value "Uplink to Spine1" \
|
||||||
|
--no-dry-run
|
||||||
|
|
||||||
|
# Set JSON value
|
||||||
|
uv run fabric-orch discover set \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config" \
|
||||||
|
--value '{"description": "Uplink", "enabled": true}' \
|
||||||
|
--no-dry-run
|
||||||
|
|
||||||
|
# Replace operation (overwrites existing config)
|
||||||
|
uv run fabric-orch discover set \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config" \
|
||||||
|
--value '{"description": "New config"}' \
|
||||||
|
--operation replace \
|
||||||
|
--no-dry-run
|
||||||
|
|
||||||
|
# Delete configuration
|
||||||
|
uv run fabric-orch discover set \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config/description" \
|
||||||
|
--value "" \
|
||||||
|
--operation delete \
|
||||||
|
--no-dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operations
|
||||||
|
|
||||||
|
| Operation | Description |
|
||||||
|
| --------- | ------------------------------------------- |
|
||||||
|
| `update` | Merge with existing configuration (default) |
|
||||||
|
| `replace` | Replace existing configuration at path |
|
||||||
|
| `delete` | Delete configuration at path |
|
||||||
|
|
||||||
|
### Dry-Run Mode
|
||||||
|
|
||||||
|
By default, the `set` command runs in **dry-run mode**, which shows what would be changed without applying it. This is a safety feature.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry-run output
|
||||||
|
⚠ DRY-RUN MODE - No changes will be applied
|
||||||
|
|
||||||
|
SET (dry-run) /interfaces/interface[name=Ethernet1]/config/description
|
||||||
|
|
||||||
|
Operation: update
|
||||||
|
Value: Uplink to Spine1
|
||||||
|
|
||||||
|
Use --no-dry-run to apply this change
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## discover subscribe
|
||||||
|
|
||||||
|
Subscribe to YANG path updates in real-time.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fabric-orch discover subscribe [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
| -------------------------- | ----- | ------------------------------------------------------------ |
|
||||||
|
| `--target` | `-t` | Target device (host:port) |
|
||||||
|
| `--username` | `-u` | Username for authentication |
|
||||||
|
| `--password` | `-p` | Password for authentication |
|
||||||
|
| `--insecure/--no-insecure` | | Skip TLS verification |
|
||||||
|
| `--output` | `-o` | Output format: pretty, json, file:path |
|
||||||
|
| `--path` | `-P` | YANG path(s) to subscribe to (can repeat) **(required)** |
|
||||||
|
| `--mode` | `-m` | Mode: on-change, sample, target-defined (default: on-change) |
|
||||||
|
| `--interval` | `-i` | Sample interval in seconds (for sample mode) |
|
||||||
|
| `--count` | `-c` | Number of updates before exiting |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Subscribe to interface operational status changes
|
||||||
|
uv run fabric-orch discover subscribe \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface/state/oper-status" \
|
||||||
|
--mode on-change
|
||||||
|
|
||||||
|
# Sample interface counters every 10 seconds
|
||||||
|
uv run fabric-orch discover subscribe \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/state/counters" \
|
||||||
|
--mode sample \
|
||||||
|
--interval 10
|
||||||
|
|
||||||
|
# Subscribe to multiple paths
|
||||||
|
uv run fabric-orch discover subscribe \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface/state/oper-status" \
|
||||||
|
--path "/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state/session-state"
|
||||||
|
|
||||||
|
# Get only 5 updates then exit
|
||||||
|
uv run fabric-orch discover subscribe \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface/state/counters" \
|
||||||
|
--mode sample \
|
||||||
|
--interval 5 \
|
||||||
|
--count 5
|
||||||
|
|
||||||
|
# Output as JSON (useful for piping)
|
||||||
|
uv run fabric-orch discover subscribe \
|
||||||
|
--target 172.16.0.50:6030 \
|
||||||
|
--path "/interfaces/interface/state/oper-status" \
|
||||||
|
--output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscription Modes
|
||||||
|
|
||||||
|
| Mode | Description |
|
||||||
|
| ---------------- | ------------------------------------------------- |
|
||||||
|
| `on-change` | Receive updates only when values change (default) |
|
||||||
|
| `sample` | Receive periodic updates at specified interval |
|
||||||
|
| `target-defined` | Let the device decide the update strategy |
|
||||||
|
|
||||||
|
### Important Note for Arista EOS
|
||||||
|
|
||||||
|
For **ON_CHANGE** subscriptions on Arista EOS, use native paths **without** module prefixes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Correct - native path
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/state"
|
||||||
|
|
||||||
|
# ❌ Incorrect - with module prefix (may fail)
|
||||||
|
--path "/openconfig-interfaces:interfaces/interface[name=Ethernet1]/state"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stopping Subscriptions
|
||||||
|
|
||||||
|
Press `Ctrl+C` to stop a subscription gracefully.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## discover paths
|
||||||
|
|
||||||
|
Display a reference table of commonly used YANG paths for Arista EOS.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover paths
|
||||||
|
```
|
||||||
|
|
||||||
|
This command doesn't require a target connection - it displays a static reference.
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Common YANG Paths for Arista EOS
|
||||||
|
|
||||||
|
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Category ┃ Path ┃ Notes ┃
|
||||||
|
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ Interfaces │ /interfaces/interface[name=Ethernet1]/state │ Full interface state │
|
||||||
|
│ Interfaces │ /interfaces/interface[name=Ethernet1]/state/oper-status │ Operational status │
|
||||||
|
│ Interfaces │ /interfaces/interface[name=Ethernet1]/state/counters │ Interface counters │
|
||||||
|
│ Loopbacks │ /interfaces/interface[name=Loopback0] │ Loopback interface │
|
||||||
|
│ VLANs │ /network-instances/network-instance[name=default]/vlans/vlan │ All VLANs │
|
||||||
|
│ BGP │ .../bgp/neighbors/neighbor/state │ All BGP neighbors │
|
||||||
|
│ VXLAN │ /interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis │ VLAN-to-VNI mappings │
|
||||||
|
│ VXLAN │ /interfaces/interface[name=Vxlan1]/arista-vxlan/config │ VXLAN config │
|
||||||
|
│ MLAG │ /arista/eos/mlag/config │ MLAG config (config only) │
|
||||||
|
│ EVPN │ /arista/eos/evpn │ EVPN config (config only) │
|
||||||
|
│ System │ /system/config/hostname │ System hostname │
|
||||||
|
└────────────┴──────────────────────────────────────────────────────────────────┴───────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
|
||||||
|
All discovery commands support three output formats via the `--output` / `-o` option:
|
||||||
|
|
||||||
|
### Pretty (default)
|
||||||
|
|
||||||
|
Rich formatted output with syntax highlighting and tables.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover get --path "/system/config" --output pretty
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
|
||||||
|
Machine-readable JSON output, suitable for piping to `jq` or other tools.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover get --path "/system/config" --output json | jq '.'
|
||||||
|
```
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
Save output directly to a file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover get --path "/interfaces/interface" --output file:interfaces.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The CLI provides helpful error messages with hints:
|
||||||
|
|
||||||
|
### Connection Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
✗ Connection Error: Failed to connect to 172.16.0.50:6030
|
||||||
|
|
||||||
|
Hints:
|
||||||
|
• Check if the target is reachable
|
||||||
|
• Verify gNMI is enabled on the device
|
||||||
|
• Check credentials
|
||||||
|
• Try --insecure flag for self-signed certs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
✗ Path Error: Path not found or invalid: /invalid/path
|
||||||
|
|
||||||
|
Hints:
|
||||||
|
• Use 'discover capabilities' to see supported models
|
||||||
|
• Check path syntax (e.g., /interfaces/interface[name=Ethernet1])
|
||||||
|
• Try a parent path first
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use Environment Variables
|
||||||
|
|
||||||
|
Set credentials once to avoid repetition:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GNMI_TARGET="172.16.0.50:6030"
|
||||||
|
export GNMI_USERNAME="admin"
|
||||||
|
export GNMI_PASSWORD="admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start with Capabilities
|
||||||
|
|
||||||
|
Before exploring paths, check what models the device supports:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover capabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Explore Incrementally
|
||||||
|
|
||||||
|
Start with broad paths and narrow down:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start broad
|
||||||
|
uv run fabric-orch discover get --path "/interfaces"
|
||||||
|
|
||||||
|
# Then narrow down
|
||||||
|
uv run fabric-orch discover get --path "/interfaces/interface[name=Ethernet1]"
|
||||||
|
|
||||||
|
# Then specific leaves
|
||||||
|
uv run fabric-orch discover get --path "/interfaces/interface[name=Ethernet1]/state/oper-status"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use Dry-Run for Set Operations
|
||||||
|
|
||||||
|
Always test with dry-run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test first
|
||||||
|
uv run fabric-orch discover set --path "..." --value "..."
|
||||||
|
|
||||||
|
# Then apply
|
||||||
|
uv run fabric-orch discover set --path "..." --value "..." --no-dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Save Exploration Results
|
||||||
|
|
||||||
|
Save interesting discoveries to files for documentation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover get --path "/arista/eos/evpn" --output file:evpn-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Use JSON Output for Scripting
|
||||||
|
|
||||||
|
For automation, use JSON output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get interface names
|
||||||
|
uv run fabric-orch discover get \
|
||||||
|
--path "/interfaces/interface" \
|
||||||
|
--output json | jq -r '.notification[0].update[0].val | keys[]'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### gNMI Not Responding
|
||||||
|
|
||||||
|
1. Verify gNMI is enabled on the device:
|
||||||
|
```
|
||||||
|
switch# show management api gnmi
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Check the correct port (default: 6030 for Arista)
|
||||||
|
|
||||||
|
3. Ensure network connectivity:
|
||||||
|
```bash
|
||||||
|
nc -zv 172.16.0.50 6030
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate Issues
|
||||||
|
|
||||||
|
For lab environments with self-signed certificates, always use `--insecure`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fabric-orch discover capabilities --target leaf1:6030 --insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Not Found
|
||||||
|
|
||||||
|
1. Check path syntax - use exact key names
|
||||||
|
2. Verify the model is supported: `discover capabilities`
|
||||||
|
3. Try the parent path first
|
||||||
|
4. Check if it's config-only or state-only data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [YANG Paths Reference](yang-paths.md) - Detailed path documentation
|
||||||
|
- [gNMI Module README](../src/gnmi/README.md) - Python API documentation
|
||||||
@@ -8,15 +8,15 @@ This document provides the complete reference of validated gNMI YANG paths for m
|
|||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
| Feature | Model Type | Config | State | Subscribe ON_CHANGE |
|
| Feature | Model Type | Config | State | Subscribe ON_CHANGE |
|
||||||
|---------|------------|--------|-------|---------------------|
|
| ---------- | ---------- | ------ | ----- | ------------------- |
|
||||||
| Interfaces | OpenConfig | ✅ | ✅ | ✅ |
|
| Interfaces | OpenConfig | ✅ | ✅ | ✅ |
|
||||||
| Loopbacks | OpenConfig | ✅ | ✅ | ✅ |
|
| Loopbacks | OpenConfig | ✅ | ✅ | ✅ |
|
||||||
| VLANs | OpenConfig | ✅ | ✅ | ✅ |
|
| VLANs | OpenConfig | ✅ | ✅ | ✅ |
|
||||||
| BGP | OpenConfig | ✅ | ✅ | ✅ |
|
| BGP | OpenConfig | ✅ | ✅ | ✅ |
|
||||||
| VXLAN | Arista Exp | ✅ | ✅ | ✅ |
|
| VXLAN | Arista Exp | ✅ | ✅ | ✅ |
|
||||||
| MLAG | Arista Exp | ✅ | ❌ | N/A |
|
| MLAG | Arista Exp | ✅ | ❌ | N/A |
|
||||||
| EVPN | Arista Exp | ✅ | ❌ | N/A |
|
| EVPN | Arista Exp | ✅ | ❌ | N/A |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,15 +24,15 @@ This document provides the complete reference of validated gNMI YANG paths for m
|
|||||||
|
|
||||||
**Model**: `openconfig-interfaces`
|
**Model**: `openconfig-interfaces`
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ------------------ | ---------------------------------------------------------- |
|
||||||
| All interfaces | `/interfaces/interface` |
|
| All interfaces | `/interfaces/interface` |
|
||||||
| Specific interface | `/interfaces/interface[name=Ethernet1]` |
|
| Specific interface | `/interfaces/interface[name=Ethernet1]` |
|
||||||
| Interface config | `/interfaces/interface[name=Ethernet1]/config` |
|
| Interface config | `/interfaces/interface[name=Ethernet1]/config` |
|
||||||
| Interface state | `/interfaces/interface[name=Ethernet1]/state` |
|
| Interface state | `/interfaces/interface[name=Ethernet1]/state` |
|
||||||
| Oper status only | `/interfaces/interface[name=Ethernet1]/state/oper-status` |
|
| Oper status only | `/interfaces/interface[name=Ethernet1]/state/oper-status` |
|
||||||
| Admin status only | `/interfaces/interface[name=Ethernet1]/state/admin-status` |
|
| Admin status only | `/interfaces/interface[name=Ethernet1]/state/admin-status` |
|
||||||
| Interface counters | `/interfaces/interface[name=Ethernet1]/state/counters` |
|
| Interface counters | `/interfaces/interface[name=Ethernet1]/state/counters` |
|
||||||
|
|
||||||
### Example: Get Interface State
|
### Example: Get Interface State
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
|
|||||||
|
|
||||||
**Model**: `openconfig-interfaces`
|
**Model**: `openconfig-interfaces`
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ---------------- | ---------------------------------------------- |
|
||||||
| Loopback0 | `/interfaces/interface[name=Loopback0]` |
|
| Loopback0 | `/interfaces/interface[name=Loopback0]` |
|
||||||
| Loopback1 (VTEP) | `/interfaces/interface[name=Loopback1]` |
|
| Loopback1 (VTEP) | `/interfaces/interface[name=Loopback1]` |
|
||||||
| Loopback config | `/interfaces/interface[name=Loopback0]/config` |
|
| Loopback config | `/interfaces/interface[name=Loopback0]/config` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -67,12 +67,12 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
|
|||||||
|
|
||||||
**Model**: `openconfig-network-instance`, `openconfig-vlan`
|
**Model**: `openconfig-network-instance`, `openconfig-vlan`
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ------------- | --------------------------------------------------------------------------------- |
|
||||||
| All VLANs | `/network-instances/network-instance[name=default]/vlans/vlan` |
|
| All VLANs | `/network-instances/network-instance[name=default]/vlans/vlan` |
|
||||||
| Specific VLAN | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]` |
|
| Specific VLAN | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]` |
|
||||||
| VLAN config | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]/config` |
|
| VLAN config | `/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=40]/config` |
|
||||||
| SVI interface | `/interfaces/interface[name=Vlan40]` |
|
| SVI interface | `/interfaces/interface[name=Vlan40]` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,18 +88,18 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
|
|||||||
|
|
||||||
### Paths
|
### Paths
|
||||||
|
|
||||||
| Operation | Path (relative to base) |
|
| Operation | Path (relative to base) |
|
||||||
|-----------|-------------------------|
|
| ---------------------- | ----------------------------------------------------------------------------------------------- |
|
||||||
| BGP global config | `/global/config` |
|
| BGP global config | `/global/config` |
|
||||||
| BGP global state | `/global/state` |
|
| BGP global state | `/global/state` |
|
||||||
| Router ID | `/global/config/router-id` |
|
| Router ID | `/global/config/router-id` |
|
||||||
| AS number | `/global/config/as` |
|
| AS number | `/global/config/as` |
|
||||||
| All neighbors | `/neighbors/neighbor` |
|
| All neighbors | `/neighbors/neighbor` |
|
||||||
| Specific neighbor | `/neighbors/neighbor[neighbor-address=10.0.1.0]` |
|
| Specific neighbor | `/neighbors/neighbor[neighbor-address=10.0.1.0]` |
|
||||||
| Neighbor state | `/neighbors/neighbor[neighbor-address=10.0.1.0]/state` |
|
| Neighbor state | `/neighbors/neighbor[neighbor-address=10.0.1.0]/state` |
|
||||||
| Neighbor session state | `/neighbors/neighbor[neighbor-address=10.0.1.0]/state/session-state` |
|
| Neighbor session state | `/neighbors/neighbor[neighbor-address=10.0.1.0]/state/session-state` |
|
||||||
| IPv4 Unicast AFI | `/neighbors/neighbor[neighbor-address=10.0.1.0]/afi-safis/afi-safi[afi-safi-name=IPV4_UNICAST]` |
|
| IPv4 Unicast AFI | `/neighbors/neighbor[neighbor-address=10.0.1.0]/afi-safis/afi-safi[afi-safi-name=IPV4_UNICAST]` |
|
||||||
| EVPN AFI | `/neighbors/neighbor[neighbor-address=10.0.1.0]/afi-safis/afi-safi[afi-safi-name=L2VPN_EVPN]` |
|
| EVPN AFI | `/neighbors/neighbor[neighbor-address=10.0.1.0]/afi-safis/afi-safi[afi-safi-name=L2VPN_EVPN]` |
|
||||||
|
|
||||||
### Example: Get All BGP Neighbors State
|
### Example: Get All BGP Neighbors State
|
||||||
|
|
||||||
@@ -123,17 +123,17 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
|
|||||||
|
|
||||||
**Model**: `arista-exp-eos-vxlan` (augments `openconfig-interfaces`)
|
**Model**: `arista-exp-eos-vxlan` (augments `openconfig-interfaces`)
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ------------------------ | ----------------------------------------------------------------------------------- |
|
||||||
| VXLAN interface | `/interfaces/interface[name=Vxlan1]` |
|
| VXLAN interface | `/interfaces/interface[name=Vxlan1]` |
|
||||||
| VXLAN augment | `/interfaces/interface[name=Vxlan1]/arista-vxlan` |
|
| VXLAN augment | `/interfaces/interface[name=Vxlan1]/arista-vxlan` |
|
||||||
| VXLAN config | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config` |
|
| VXLAN config | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config` |
|
||||||
| VXLAN state | `/interfaces/interface[name=Vxlan1]/arista-vxlan/state` |
|
| VXLAN state | `/interfaces/interface[name=Vxlan1]/arista-vxlan/state` |
|
||||||
| Source interface | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config/src-ip-intf` |
|
| Source interface | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config/src-ip-intf` |
|
||||||
| UDP port | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config/udp-port` |
|
| UDP port | `/interfaces/interface[name=Vxlan1]/arista-vxlan/config/udp-port` |
|
||||||
| All VLAN-to-VNI mappings | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis` |
|
| All VLAN-to-VNI mappings | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis` |
|
||||||
| Specific VNI mapping | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis/vlan-to-vni[vlan=40]` |
|
| Specific VNI mapping | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis/vlan-to-vni[vlan=40]` |
|
||||||
| VRF-to-VNI (L3 VNI) | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vrf-to-vnis` |
|
| VRF-to-VNI (L3 VNI) | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vrf-to-vnis` |
|
||||||
|
|
||||||
### Example: Get VXLAN VNI Mappings
|
### Example: Get VXLAN VNI Mappings
|
||||||
|
|
||||||
@@ -159,22 +159,22 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
|
|||||||
|
|
||||||
> ⚠️ **Limitation**: Only configuration is exposed via gNMI. Operational state (peer status, role, negotiation) requires eAPI.
|
> ⚠️ **Limitation**: Only configuration is exposed via gNMI. Operational state (peer status, role, negotiation) requires eAPI.
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ----------- | ------------------------- |
|
||||||
| Full MLAG | `/arista/eos/mlag` |
|
| Full MLAG | `/arista/eos/mlag` |
|
||||||
| MLAG config | `/arista/eos/mlag/config` |
|
| MLAG config | `/arista/eos/mlag/config` |
|
||||||
|
|
||||||
### Config Fields
|
### Config Fields
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|-------|-------------|
|
| ------------------------------ | ------------------------------------- |
|
||||||
| `domain-id` | MLAG domain identifier |
|
| `domain-id` | MLAG domain identifier |
|
||||||
| `local-intf` | Local VLAN interface (e.g., Vlan4090) |
|
| `local-intf` | Local VLAN interface (e.g., Vlan4090) |
|
||||||
| `peer-address` | MLAG peer IP address |
|
| `peer-address` | MLAG peer IP address |
|
||||||
| `peer-link-intf` | Peer-link port-channel |
|
| `peer-link-intf` | Peer-link port-channel |
|
||||||
| `dual-primary-action` | Action on dual-primary detection |
|
| `dual-primary-action` | Action on dual-primary detection |
|
||||||
| `dual-primary-detection-delay` | Detection delay in seconds |
|
| `dual-primary-detection-delay` | Detection delay in seconds |
|
||||||
| `heartbeat-peer-address` | Heartbeat peer IP and VRF |
|
| `heartbeat-peer-address` | Heartbeat peer IP and VRF |
|
||||||
|
|
||||||
### Example: Get MLAG Config
|
### Example: Get MLAG Config
|
||||||
|
|
||||||
@@ -200,22 +200,22 @@ curl -X POST https://switch/command-api \
|
|||||||
|
|
||||||
> ⚠️ **Limitation**: Only configuration is exposed via gNMI. Learned routes and MACs require eAPI.
|
> ⚠️ **Limitation**: Only configuration is exposed via gNMI. Learned routes and MACs require eAPI.
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ----------------- | -------------------------------------------------------- |
|
||||||
| All EVPN | `/arista/eos/evpn` |
|
| All EVPN | `/arista/eos/evpn` |
|
||||||
| EVPN instances | `/arista/eos/evpn/evpn-instances` |
|
| EVPN instances | `/arista/eos/evpn/evpn-instances` |
|
||||||
| Specific instance | `/arista/eos/evpn/evpn-instances/evpn-instance[name=40]` |
|
| Specific instance | `/arista/eos/evpn/evpn-instances/evpn-instance[name=40]` |
|
||||||
|
|
||||||
### Config Fields
|
### Config Fields
|
||||||
|
|
||||||
| Field | Path |
|
| Field | Path |
|
||||||
|-------|------|
|
| ------------------- | ------------------------------------------------------ |
|
||||||
| Name | `.../evpn-instance[name=X]/config/name` |
|
| Name | `.../evpn-instance[name=X]/config/name` |
|
||||||
| Route Distinguisher | `.../evpn-instance[name=X]/config/route-distinguisher` |
|
| Route Distinguisher | `.../evpn-instance[name=X]/config/route-distinguisher` |
|
||||||
| Redistribute | `.../evpn-instance[name=X]/config/redistribute` |
|
| Redistribute | `.../evpn-instance[name=X]/config/redistribute` |
|
||||||
| Route Target Import | `.../evpn-instance[name=X]/route-target/config/import` |
|
| Route Target Import | `.../evpn-instance[name=X]/route-target/config/import` |
|
||||||
| Route Target Export | `.../evpn-instance[name=X]/route-target/config/export` |
|
| Route Target Export | `.../evpn-instance[name=X]/route-target/config/export` |
|
||||||
| VLANs | `.../evpn-instance[name=X]/vlans/vlan` |
|
| VLANs | `.../evpn-instance[name=X]/vlans/vlan` |
|
||||||
|
|
||||||
### Example: Get EVPN Config
|
### Example: Get EVPN Config
|
||||||
|
|
||||||
@@ -230,12 +230,12 @@ gnmic -a 172.16.0.50:6030 -u admin -p admin --insecure \
|
|||||||
|
|
||||||
**Model**: `openconfig-interfaces`, `openconfig-if-aggregate`
|
**Model**: `openconfig-interfaces`, `openconfig-if-aggregate`
|
||||||
|
|
||||||
| Operation | Path |
|
| Operation | Path |
|
||||||
|-----------|------|
|
| ---------------------- | -------------------------------------------------------------------- |
|
||||||
| Port-Channel interface | `/interfaces/interface[name=Port-Channel999]` |
|
| Port-Channel interface | `/interfaces/interface[name=Port-Channel999]` |
|
||||||
| LAG config | `/interfaces/interface[name=Port-Channel999]/aggregation/config` |
|
| LAG config | `/interfaces/interface[name=Port-Channel999]/aggregation/config` |
|
||||||
| LAG state | `/interfaces/interface[name=Port-Channel999]/aggregation/state` |
|
| LAG state | `/interfaces/interface[name=Port-Channel999]/aggregation/state` |
|
||||||
| Member interfaces | `/interfaces/interface[name=Ethernet1]/ethernet/config/aggregate-id` |
|
| Member interfaces | `/interfaces/interface[name=Ethernet1]/ethernet/config/aggregate-id` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,33 @@ description = "Declarative Network Fabric Orchestrator - Terraform-like infrastr
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dataclasses>=0.8",
|
"click>=8.1.0",
|
||||||
"typing>=3.10.0.0",
|
"pygnmi>=0.8.0",
|
||||||
|
"rich>=13.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
fabric-orch = "src.cli:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["src"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "W"]
|
||||||
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"ruff>=0.14.10",
|
||||||
]
|
]
|
||||||
|
|||||||
3
src/__init__.py
Normal file
3
src/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Fabric Orchestrator source package.
|
||||||
|
"""
|
||||||
568
src/cli.py
Normal file
568
src/cli.py
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
"""
|
||||||
|
Fabric Orchestrator CLI.
|
||||||
|
|
||||||
|
This module provides the CLI for fabric-orchestrator using Click.
|
||||||
|
Main command groups:
|
||||||
|
- discover: YANG path exploration and discovery tools
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import click
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.json import JSON
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from src.gnmi import GNMIClient, GNMIConnectionError, GNMIError, GNMIPathError
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Constants
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
error_console = Console(stderr=True)
|
||||||
|
|
||||||
|
# Environment variable defaults
|
||||||
|
ENV_TARGET = "GNMI_TARGET"
|
||||||
|
ENV_USERNAME = "GNMI_USERNAME"
|
||||||
|
ENV_PASSWORD = "GNMI_PASSWORD"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Output Handlers
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def output_result(
|
||||||
|
data: Any,
|
||||||
|
output_format: str = "pretty",
|
||||||
|
title: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Output result in specified format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Data to output
|
||||||
|
output_format: Format - "pretty", "json", or "file:path"
|
||||||
|
title: Optional title for pretty output
|
||||||
|
"""
|
||||||
|
# Convert to JSON string
|
||||||
|
json_str = json.dumps(data, indent=2, default=str)
|
||||||
|
|
||||||
|
if output_format == "json":
|
||||||
|
# Raw JSON to stdout
|
||||||
|
click.echo(json_str)
|
||||||
|
elif output_format.startswith("file:"):
|
||||||
|
# Save to file
|
||||||
|
file_path = output_format[5:]
|
||||||
|
Path(file_path).write_text(json_str)
|
||||||
|
console.print(f"[green]✓[/green] Results saved to [bold]{file_path}[/bold]")
|
||||||
|
else:
|
||||||
|
# Pretty print with rich
|
||||||
|
if title:
|
||||||
|
console.print(Panel(JSON(json_str), title=title, border_style="blue"))
|
||||||
|
else:
|
||||||
|
console.print(JSON(json_str))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_target(target: str) -> tuple[str, int]:
|
||||||
|
"""Parse target string into host and port."""
|
||||||
|
if ":" in target:
|
||||||
|
host, port_str = target.rsplit(":", 1)
|
||||||
|
return host, int(port_str)
|
||||||
|
return target, 6030
|
||||||
|
|
||||||
|
|
||||||
|
def handle_error(e: Exception, context: str = "") -> None:
|
||||||
|
"""Handle and display error."""
|
||||||
|
if isinstance(e, GNMIConnectionError):
|
||||||
|
error_console.print(f"[red]✗ Connection Error:[/red] {e}")
|
||||||
|
error_console.print(
|
||||||
|
"\n[dim]Hints:[/dim]\n"
|
||||||
|
" • Check if the target is reachable\n"
|
||||||
|
" • Verify gNMI is enabled on the device\n"
|
||||||
|
" • Check credentials\n"
|
||||||
|
" • Try --insecure flag for self-signed certs"
|
||||||
|
)
|
||||||
|
elif isinstance(e, GNMIPathError):
|
||||||
|
error_console.print(f"[red]✗ Path Error:[/red] {e}")
|
||||||
|
error_console.print(
|
||||||
|
"\n[dim]Hints:[/dim]\n"
|
||||||
|
" • Use 'discover capabilities' to see supported models\n"
|
||||||
|
" • Check path syntax (e.g., /interfaces/interface[name=Ethernet1])\n"
|
||||||
|
" • Try a parent path first"
|
||||||
|
)
|
||||||
|
elif isinstance(e, GNMIError):
|
||||||
|
error_console.print(f"[red]✗ gNMI Error:[/red] {e}")
|
||||||
|
else:
|
||||||
|
error_console.print(f"[red]✗ Error:[/red] {e}")
|
||||||
|
|
||||||
|
if context:
|
||||||
|
error_console.print(f"[dim]Context: {context}[/dim]")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CLI Groups
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version="0.1.0", prog_name="fabric-orch")
|
||||||
|
def cli():
|
||||||
|
"""
|
||||||
|
Fabric Orchestrator - Terraform-like management for Arista EVPN-VXLAN fabrics.
|
||||||
|
Use 'fabric-orch discover' commands to explore YANG paths on devices.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def discover():
|
||||||
|
"""
|
||||||
|
YANG path discovery and exploration tools.
|
||||||
|
|
||||||
|
Use these commands to explore YANG paths, get device capabilities,
|
||||||
|
and interactively discover configuration and state data.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Common Options
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def common_options(f):
|
||||||
|
"""Common options for all discover commands."""
|
||||||
|
f = click.option(
|
||||||
|
"--target", "-t",
|
||||||
|
envvar=ENV_TARGET,
|
||||||
|
required=True,
|
||||||
|
help="Target device (host:port). Env: GNMI_TARGET",
|
||||||
|
)(f)
|
||||||
|
f = click.option(
|
||||||
|
"--username", "-u",
|
||||||
|
envvar=ENV_USERNAME,
|
||||||
|
default="admin",
|
||||||
|
help="Username for authentication. Env: GNMI_USERNAME",
|
||||||
|
)(f)
|
||||||
|
f = click.option(
|
||||||
|
"--password", "-p",
|
||||||
|
envvar=ENV_PASSWORD,
|
||||||
|
default="admin",
|
||||||
|
help="Password for authentication. Env: GNMI_PASSWORD",
|
||||||
|
)(f)
|
||||||
|
f = click.option(
|
||||||
|
"--insecure/--no-insecure",
|
||||||
|
default=True,
|
||||||
|
help="Skip TLS verification (default: True for lab)",
|
||||||
|
)(f)
|
||||||
|
f = click.option(
|
||||||
|
"--output", "-o",
|
||||||
|
default="pretty",
|
||||||
|
help="Output format: pretty, json, or file:path",
|
||||||
|
)(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Discover Commands
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@discover.command("capabilities")
|
||||||
|
@common_options
|
||||||
|
def discover_capabilities(
|
||||||
|
target: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
insecure: bool,
|
||||||
|
output: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
List device capabilities and supported YANG models.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
fabric-orch discover capabilities --target leaf1:6030
|
||||||
|
"""
|
||||||
|
host, port = parse_target(target)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with GNMIClient(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
insecure=insecure,
|
||||||
|
) as client:
|
||||||
|
caps = client.capabilities()
|
||||||
|
|
||||||
|
if output == "pretty":
|
||||||
|
# Pretty print capabilities
|
||||||
|
console.print(
|
||||||
|
f"\n[bold blue]Device Capabilities[/bold blue] - {target}\n"
|
||||||
|
)
|
||||||
|
console.print(f"gNMI Version: [green]{caps.get('gnmi_version', 'unknown')}[/green]")
|
||||||
|
|
||||||
|
# Encodings
|
||||||
|
encodings = caps.get("supported_encodings", [])
|
||||||
|
console.print(f"Supported Encodings: [cyan]{', '.join(encodings)}[/cyan]")
|
||||||
|
|
||||||
|
# Models table
|
||||||
|
models = caps.get("supported_models", [])
|
||||||
|
if models:
|
||||||
|
console.print(f"\n[bold]Supported Models ({len(models)}):[/bold]\n")
|
||||||
|
|
||||||
|
table = Table(show_header=True, header_style="bold")
|
||||||
|
table.add_column("Model Name", style="cyan")
|
||||||
|
table.add_column("Organization")
|
||||||
|
table.add_column("Version", style="green")
|
||||||
|
|
||||||
|
for model in sorted(models, key=lambda x: x.get("name", "")):
|
||||||
|
table.add_row(
|
||||||
|
model.get("name", ""),
|
||||||
|
model.get("organization", ""),
|
||||||
|
model.get("version", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
else:
|
||||||
|
output_result(caps, output, "Device Capabilities")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
handle_error(e, f"Getting capabilities from {target}")
|
||||||
|
|
||||||
|
|
||||||
|
@discover.command("get")
|
||||||
|
@common_options
|
||||||
|
@click.option(
|
||||||
|
"--path", "-P",
|
||||||
|
required=True,
|
||||||
|
help="YANG path to get data from",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--type", "-T",
|
||||||
|
"data_type",
|
||||||
|
type=click.Choice(["config", "state", "all"]),
|
||||||
|
default="all",
|
||||||
|
help="Type of data to retrieve (default: all)",
|
||||||
|
)
|
||||||
|
def discover_get(
|
||||||
|
target: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
insecure: bool,
|
||||||
|
output: str,
|
||||||
|
path: str,
|
||||||
|
data_type: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get configuration or state data at a YANG path.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Get all data at path
|
||||||
|
fabric-orch discover get --target leaf1:6030 --path "/interfaces/interface[name=Ethernet1]"
|
||||||
|
|
||||||
|
# Get config only
|
||||||
|
fabric-orch discover get --target leaf1:6030 --path "/interfaces" --type config
|
||||||
|
|
||||||
|
# Get state only
|
||||||
|
fabric-orch discover get --target leaf1:6030 --path "/interfaces/interface[name=Ethernet1]/state" --type state
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
fabric-orch discover get --target leaf1:6030 --path "/" --output file:result.json
|
||||||
|
"""
|
||||||
|
host, port = parse_target(target)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with GNMIClient(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
insecure=insecure,
|
||||||
|
) as client:
|
||||||
|
result = client.get(path, data_type=data_type)
|
||||||
|
|
||||||
|
if output == "pretty":
|
||||||
|
console.print(
|
||||||
|
f"\n[bold blue]GET {path}[/bold blue] ({data_type})\n"
|
||||||
|
)
|
||||||
|
output_result(result, "pretty")
|
||||||
|
else:
|
||||||
|
output_result(result, output, f"GET {path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
handle_error(e, f"Getting data at {path}")
|
||||||
|
|
||||||
|
|
||||||
|
@discover.command("set")
|
||||||
|
@common_options
|
||||||
|
@click.option(
|
||||||
|
"--path", "-P",
|
||||||
|
required=True,
|
||||||
|
help="YANG path to set",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--value", "-v",
|
||||||
|
required=True,
|
||||||
|
help="Value to set (JSON string or simple value)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--operation", "-O",
|
||||||
|
type=click.Choice(["update", "replace", "delete"]),
|
||||||
|
default="update",
|
||||||
|
help="Set operation type (default: update)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dry-run/--no-dry-run",
|
||||||
|
default=True,
|
||||||
|
help="Dry-run mode - don't apply changes (default: True)",
|
||||||
|
)
|
||||||
|
def discover_set(
|
||||||
|
target: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
insecure: bool,
|
||||||
|
output: str,
|
||||||
|
path: str,
|
||||||
|
value: str,
|
||||||
|
operation: str,
|
||||||
|
dry_run: bool,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set configuration at a YANG path.
|
||||||
|
|
||||||
|
By default runs in dry-run mode. Use --no-dry-run to actually apply changes.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Dry-run (default)
|
||||||
|
fabric-orch discover set --target leaf1:6030 \\
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config/description" \\
|
||||||
|
--value "Uplink to Spine"
|
||||||
|
|
||||||
|
# Actually apply
|
||||||
|
fabric-orch discover set --target leaf1:6030 \\
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config/description" \\
|
||||||
|
--value "Uplink to Spine" \\
|
||||||
|
--no-dry-run
|
||||||
|
|
||||||
|
# Delete config
|
||||||
|
fabric-orch discover set --target leaf1:6030 \\
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/config/description" \\
|
||||||
|
--value "" --operation delete --no-dry-run
|
||||||
|
"""
|
||||||
|
host, port = parse_target(target)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
console.print("[yellow]⚠ DRY-RUN MODE[/yellow] - No changes will be applied\n")
|
||||||
|
else:
|
||||||
|
console.print("[red]⚠ LIVE MODE[/red] - Changes WILL be applied!\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with GNMIClient(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
insecure=insecure,
|
||||||
|
) as client:
|
||||||
|
result = client.set(
|
||||||
|
path=path,
|
||||||
|
value=value,
|
||||||
|
operation=operation,
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
if output == "pretty":
|
||||||
|
if dry_run:
|
||||||
|
console.print(f"[bold blue]SET (dry-run)[/bold blue] {path}\n")
|
||||||
|
console.print(f"Operation: [cyan]{operation}[/cyan]")
|
||||||
|
console.print(f"Value: [green]{value}[/green]")
|
||||||
|
console.print("\n[dim]Use --no-dry-run to apply this change[/dim]")
|
||||||
|
else:
|
||||||
|
console.print(f"[bold green]SET (applied)[/bold green] {path}\n")
|
||||||
|
output_result(result, "pretty")
|
||||||
|
else:
|
||||||
|
output_result(result, output, f"SET {path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
handle_error(e, f"Setting data at {path}")
|
||||||
|
|
||||||
|
|
||||||
|
@discover.command("subscribe")
|
||||||
|
@common_options
|
||||||
|
@click.option(
|
||||||
|
"--path", "-P",
|
||||||
|
required=True,
|
||||||
|
multiple=True,
|
||||||
|
help="YANG path(s) to subscribe to (can specify multiple)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--mode", "-m",
|
||||||
|
type=click.Choice(["on-change", "sample", "target-defined"]),
|
||||||
|
default="on-change",
|
||||||
|
help="Subscription mode (default: on-change)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--interval", "-i",
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help="Sample interval in seconds (for sample mode)",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--count", "-c",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Number of updates to receive before exiting",
|
||||||
|
)
|
||||||
|
def discover_subscribe(
|
||||||
|
target: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
insecure: bool,
|
||||||
|
output: str,
|
||||||
|
path: tuple[str, ...],
|
||||||
|
mode: str,
|
||||||
|
interval: int,
|
||||||
|
count: int | None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Subscribe to YANG path updates.
|
||||||
|
|
||||||
|
Note: For Arista EOS, use native paths WITHOUT module prefixes
|
||||||
|
for ON_CHANGE subscriptions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Subscribe to interface state changes
|
||||||
|
fabric-orch discover subscribe --target leaf1:6030 \\
|
||||||
|
--path "/interfaces/interface/state/oper-status" \\
|
||||||
|
--mode on-change
|
||||||
|
|
||||||
|
# Sample interface counters every 10 seconds
|
||||||
|
fabric-orch discover subscribe --target leaf1:6030 \\
|
||||||
|
--path "/interfaces/interface[name=Ethernet1]/state/counters" \\
|
||||||
|
--mode sample --interval 10
|
||||||
|
|
||||||
|
# Subscribe to multiple paths
|
||||||
|
fabric-orch discover subscribe --target leaf1:6030 \\
|
||||||
|
--path "/interfaces/interface/state/oper-status" \\
|
||||||
|
--path "/network-instances/network-instance/protocols/protocol/bgp/neighbors/neighbor/state/session-state"
|
||||||
|
"""
|
||||||
|
host, port = parse_target(target)
|
||||||
|
paths = list(path)
|
||||||
|
|
||||||
|
console.print(f"\n[bold blue]Subscribing to {len(paths)} path(s)[/bold blue]\n")
|
||||||
|
for p in paths:
|
||||||
|
console.print(f" • {p}")
|
||||||
|
console.print(f"\nMode: [cyan]{mode}[/cyan]")
|
||||||
|
if mode == "sample":
|
||||||
|
console.print(f"Interval: [cyan]{interval}s[/cyan]")
|
||||||
|
console.print("\n[dim]Press Ctrl+C to stop[/dim]\n")
|
||||||
|
console.print("─" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with GNMIClient(
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
insecure=insecure,
|
||||||
|
) as client:
|
||||||
|
subscription = client.subscribe(
|
||||||
|
paths=paths,
|
||||||
|
mode="stream",
|
||||||
|
stream_mode=mode,
|
||||||
|
sample_interval=interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_count = 0
|
||||||
|
try:
|
||||||
|
for update in subscription:
|
||||||
|
update_count += 1
|
||||||
|
|
||||||
|
if output == "pretty":
|
||||||
|
console.print(f"\n[green]Update #{update_count}[/green]")
|
||||||
|
output_result(update, "pretty")
|
||||||
|
else:
|
||||||
|
output_result(update, output)
|
||||||
|
|
||||||
|
if count is not None and update_count >= count:
|
||||||
|
console.print(f"\n[yellow]Reached {count} updates, stopping[/yellow]")
|
||||||
|
break
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print(f"\n\n[yellow]Stopped after {update_count} updates[/yellow]")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
handle_error(e, f"Subscribing to {paths}")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Additional Commands
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@discover.command("paths")
|
||||||
|
def discover_paths():
|
||||||
|
"""
|
||||||
|
Show commonly used YANG paths for Arista EOS.
|
||||||
|
|
||||||
|
This displays a reference of validated paths that can be used
|
||||||
|
with the get, set, and subscribe commands.
|
||||||
|
"""
|
||||||
|
console.print("\n[bold blue]Common YANG Paths for Arista EOS[/bold blue]\n")
|
||||||
|
|
||||||
|
table = Table(show_header=True, header_style="bold")
|
||||||
|
table.add_column("Category", style="cyan")
|
||||||
|
table.add_column("Path")
|
||||||
|
table.add_column("Notes", style="dim")
|
||||||
|
|
||||||
|
paths = [
|
||||||
|
("Interfaces", "/interfaces/interface[name=Ethernet1]/state", "Full interface state"),
|
||||||
|
("Interfaces", "/interfaces/interface[name=Ethernet1]/state/oper-status", "Operational status"),
|
||||||
|
("Interfaces", "/interfaces/interface[name=Ethernet1]/state/counters", "Interface counters"),
|
||||||
|
("Loopbacks", "/interfaces/interface[name=Loopback0]", "Loopback interface"),
|
||||||
|
("VLANs", "/network-instances/network-instance[name=default]/vlans/vlan", "All VLANs"),
|
||||||
|
("BGP", "/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state", "All BGP neighbors"),
|
||||||
|
("VXLAN", "/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis", "VLAN-to-VNI mappings"),
|
||||||
|
("VXLAN", "/interfaces/interface[name=Vxlan1]/arista-vxlan/config", "VXLAN config"),
|
||||||
|
("MLAG", "/arista/eos/mlag/config", "MLAG config (config only)"),
|
||||||
|
("EVPN", "/arista/eos/evpn", "EVPN config (config only)"),
|
||||||
|
("System", "/system/config/hostname", "System hostname"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for category, path, notes in paths:
|
||||||
|
table.add_row(category, path, notes)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
console.print("\n[bold]Path Syntax Tips:[/bold]")
|
||||||
|
console.print(" • Use brackets for keys: [name=Ethernet1]")
|
||||||
|
console.print(" • Multiple keys: [identifier=BGP][name=BGP]")
|
||||||
|
console.print(" • Wildcards not supported in GET, but can omit keys for all")
|
||||||
|
console.print("\n[bold]Subscription Notes:[/bold]")
|
||||||
|
console.print(" • Use native paths WITHOUT module prefix for ON_CHANGE")
|
||||||
|
console.print(" • ✅ /interfaces/interface[name=Ethernet1]/state")
|
||||||
|
console.print(" • ❌ /openconfig-interfaces:interfaces/...")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Entry Point
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
cli()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
183
src/gnmi/README.md
Normal file
183
src/gnmi/README.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# gNMI Client Module
|
||||||
|
|
||||||
|
This module provides a high-level gNMI client wrapper for interacting with Arista devices using pygnmi.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Connection management with context manager support
|
||||||
|
- Get capabilities (list supported YANG models)
|
||||||
|
- Get configuration and state data at any YANG path
|
||||||
|
- Set configuration with dry-run support
|
||||||
|
- Subscribe to path updates (on-change, sample modes)
|
||||||
|
- Error handling with meaningful exceptions
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Ensure you have the required dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv add pygnmi click rich
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Connection
|
||||||
|
|
||||||
|
```python
|
||||||
|
from gnmi import GNMIClient
|
||||||
|
|
||||||
|
# Using context manager (recommended)
|
||||||
|
with GNMIClient(
|
||||||
|
host="172.16.0.50",
|
||||||
|
port=6030,
|
||||||
|
username="admin",
|
||||||
|
password="admin",
|
||||||
|
insecure=True
|
||||||
|
) as client:
|
||||||
|
# Use the client
|
||||||
|
caps = client.capabilities()
|
||||||
|
print(caps)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Capabilities
|
||||||
|
|
||||||
|
```python
|
||||||
|
with GNMIClient(host="172.16.0.50", port=6030) as client:
|
||||||
|
# Get raw capabilities
|
||||||
|
caps = client.capabilities()
|
||||||
|
print(f"gNMI Version: {caps['gnmi_version']}")
|
||||||
|
|
||||||
|
# Get parsed models
|
||||||
|
models = client.get_models()
|
||||||
|
for model in models:
|
||||||
|
print(f" {model.name} ({model.organization}) v{model.version}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Data
|
||||||
|
|
||||||
|
```python
|
||||||
|
with GNMIClient(host="172.16.0.50", port=6030) as client:
|
||||||
|
# Get all data (config + state)
|
||||||
|
result = client.get("/interfaces/interface[name=Ethernet1]")
|
||||||
|
|
||||||
|
# Get config only
|
||||||
|
result = client.get(
|
||||||
|
"/interfaces/interface[name=Ethernet1]",
|
||||||
|
data_type="config"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get state only
|
||||||
|
result = client.get(
|
||||||
|
"/interfaces/interface[name=Ethernet1]/state",
|
||||||
|
data_type="state"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get multiple paths
|
||||||
|
result = client.get([
|
||||||
|
"/interfaces/interface[name=Ethernet1]/state",
|
||||||
|
"/interfaces/interface[name=Ethernet2]/state",
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
with GNMIClient(host="172.16.0.50", port=6030) as client:
|
||||||
|
# Dry-run (default) - shows what would be set
|
||||||
|
result = client.set(
|
||||||
|
path="/interfaces/interface[name=Ethernet1]/config/description",
|
||||||
|
value="Uplink to Spine",
|
||||||
|
dry_run=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Actually apply the change
|
||||||
|
result = client.set(
|
||||||
|
path="/interfaces/interface[name=Ethernet1]/config/description",
|
||||||
|
value="Uplink to Spine",
|
||||||
|
dry_run=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace operation
|
||||||
|
result = client.set(
|
||||||
|
path="/interfaces/interface[name=Ethernet1]/config",
|
||||||
|
value={"description": "New config", "enabled": True},
|
||||||
|
operation="replace",
|
||||||
|
dry_run=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete operation
|
||||||
|
result = client.set(
|
||||||
|
path="/interfaces/interface[name=Ethernet1]/config/description",
|
||||||
|
value=None,
|
||||||
|
operation="delete",
|
||||||
|
dry_run=False
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscribe to Updates
|
||||||
|
|
||||||
|
```python
|
||||||
|
with GNMIClient(host="172.16.0.50", port=6030) as client:
|
||||||
|
# Subscribe to interface state changes
|
||||||
|
subscription = client.subscribe(
|
||||||
|
paths="/interfaces/interface/state/oper-status",
|
||||||
|
mode="stream",
|
||||||
|
stream_mode="on-change"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process updates
|
||||||
|
for update in subscription:
|
||||||
|
print(update)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The module provides specific exceptions for different error types:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from gnmi import GNMIClient, GNMIError, GNMIConnectionError, GNMIPathError
|
||||||
|
|
||||||
|
try:
|
||||||
|
with GNMIClient(host="172.16.0.50", port=6030) as client:
|
||||||
|
result = client.get("/invalid/path")
|
||||||
|
except GNMIConnectionError as e:
|
||||||
|
print(f"Connection failed: {e}")
|
||||||
|
except GNMIPathError as e:
|
||||||
|
print(f"Invalid path: {e}")
|
||||||
|
except GNMIError as e:
|
||||||
|
print(f"gNMI error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The CLI supports these environment variables for defaults:
|
||||||
|
|
||||||
|
- `GNMI_TARGET` - Target device (host:port)
|
||||||
|
- `GNMI_USERNAME` - Username for authentication
|
||||||
|
- `GNMI_PASSWORD` - Password for authentication
|
||||||
|
|
||||||
|
## Path Format Notes
|
||||||
|
|
||||||
|
For Arista EOS, use native paths **without** module prefixes for subscriptions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Correct - native path
|
||||||
|
"/interfaces/interface[name=Ethernet1]/state"
|
||||||
|
|
||||||
|
# ❌ Incorrect - with module prefix (may fail for subscriptions)
|
||||||
|
"/openconfig-interfaces:interfaces/interface[name=Ethernet1]/state"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validated Paths
|
||||||
|
|
||||||
|
These paths have been validated against Arista cEOS 4.35.0F:
|
||||||
|
|
||||||
|
| Category | Path | Notes |
|
||||||
|
|----------|------|-------|
|
||||||
|
| Interfaces | `/interfaces/interface[name=Ethernet1]/state` | Full state |
|
||||||
|
| BGP | `/network-instances/network-instance[name=default]/protocols/protocol[identifier=BGP][name=BGP]/bgp/neighbors/neighbor/state` | All neighbors |
|
||||||
|
| VXLAN | `/interfaces/interface[name=Vxlan1]/arista-vxlan/vlan-to-vnis` | VNI mappings |
|
||||||
|
| MLAG | `/arista/eos/mlag/config` | Config only |
|
||||||
|
| EVPN | `/arista/eos/evpn` | Config only |
|
||||||
|
|
||||||
|
See `docs/yang-paths.md` for complete path documentation.
|
||||||
23
src/gnmi/__init__.py
Normal file
23
src/gnmi/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
gNMI Client Module for Fabric Orchestrator.
|
||||||
|
|
||||||
|
This module provides a gNMI client wrapper for interacting with
|
||||||
|
Arista devices using pygnmi.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from gnmi import GNMIClient
|
||||||
|
|
||||||
|
with GNMIClient(
|
||||||
|
host="leaf1",
|
||||||
|
port=6030,
|
||||||
|
username="admin",
|
||||||
|
password="admin",
|
||||||
|
insecure=True
|
||||||
|
) as client:
|
||||||
|
caps = client.capabilities()
|
||||||
|
print(caps)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .client import GNMIClient, GNMIConnectionError, GNMIError, GNMIPathError
|
||||||
|
|
||||||
|
__all__ = ["GNMIClient", "GNMIError", "GNMIConnectionError", "GNMIPathError"]
|
||||||
383
src/gnmi/client.py
Normal file
383
src/gnmi/client.py
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
"""
|
||||||
|
gNMI Client Wrapper for Fabric Orchestrator.
|
||||||
|
|
||||||
|
This module provides a high-level gNMI client using pygnmi with:
|
||||||
|
- Connection management
|
||||||
|
- Error handling with meaningful exceptions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pygnmi.client import gNMIclient
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Exceptions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class GNMIError(Exception):
|
||||||
|
"""Base exception for gNMI operations."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GNMIConnectionError(GNMIError):
|
||||||
|
"""Raised when connection to device fails."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GNMIPathError(GNMIError):
|
||||||
|
"""Raised when path is invalid or not found."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Data Types
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
DataType = Literal["config", "state", "all"]
|
||||||
|
SetOperation = Literal["update", "replace", "delete"]
|
||||||
|
SubscribeMode = Literal["once", "stream", "poll"]
|
||||||
|
StreamMode = Literal["on-change", "sample", "target-defined"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Capability:
|
||||||
|
"""Represents a gNMI capability/model."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
organization: str
|
||||||
|
version: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name} ({self.organization}) v{self.version}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubscriptionUpdate:
|
||||||
|
"""Represents a subscription update."""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
value: Any
|
||||||
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# gNMI Client
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class GNMIClient:
|
||||||
|
"""
|
||||||
|
gNMI Client wrapper using pygnmi.
|
||||||
|
|
||||||
|
Provides high-level methods for gNMI operations with proper
|
||||||
|
error handling and context manager support.
|
||||||
|
|
||||||
|
Or synchronously:
|
||||||
|
with GNMIClient(host="leaf1", port=6030, ...) as client:
|
||||||
|
caps = client.capabilities()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int = 6030,
|
||||||
|
username: str = "admin",
|
||||||
|
password: str = "admin",
|
||||||
|
insecure: bool = True,
|
||||||
|
skip_verify: bool = True,
|
||||||
|
timeout: int = 10,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize gNMI client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: Target hostname or IP
|
||||||
|
port: gNMI port (default: 6030)
|
||||||
|
username: Username for authentication
|
||||||
|
password: Password for authentication
|
||||||
|
insecure: Skip TLS verification (default: True for lab)
|
||||||
|
skip_verify: Skip certificate verification
|
||||||
|
timeout: Connection timeout in seconds
|
||||||
|
"""
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.insecure = insecure
|
||||||
|
self.skip_verify = skip_verify
|
||||||
|
self.timeout = timeout
|
||||||
|
self._client: gNMIclient | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target(self) -> str:
|
||||||
|
"""Get target string (host:port)."""
|
||||||
|
return f"{self.host}:{self.port}"
|
||||||
|
|
||||||
|
def _ensure_connected(self) -> gNMIclient:
|
||||||
|
"""Ensure client is connected."""
|
||||||
|
if self._client is None:
|
||||||
|
raise GNMIConnectionError("Client not connected. Use context manager.")
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def __enter__(self) -> GNMIClient:
|
||||||
|
"""Enter context manager (sync)."""
|
||||||
|
try:
|
||||||
|
self._client = gNMIclient(
|
||||||
|
target=(self.host, self.port),
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
insecure=self.insecure,
|
||||||
|
skip_verify=self.skip_verify,
|
||||||
|
)
|
||||||
|
self._client.__enter__()
|
||||||
|
return self
|
||||||
|
except Exception as e:
|
||||||
|
raise GNMIConnectionError(
|
||||||
|
f"Failed to connect to {self.target}: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||||
|
"""Exit context manager (sync)."""
|
||||||
|
if self._client:
|
||||||
|
try:
|
||||||
|
self._client.__exit__(exc_type, exc_val, exc_tb)
|
||||||
|
except Exception:
|
||||||
|
pass # Ignore cleanup errors
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# gNMI Operations
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def capabilities(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get device capabilities.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- gnmi_version: gNMI protocol version
|
||||||
|
- supported_models: List of supported YANG models
|
||||||
|
- supported_encodings: List of supported encodings
|
||||||
|
"""
|
||||||
|
client = self._ensure_connected()
|
||||||
|
try:
|
||||||
|
result = client.capabilities()
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
raise GNMIError(f"Failed to get capabilities: {e}") from e
|
||||||
|
|
||||||
|
def get_models(self) -> list[Capability]:
|
||||||
|
"""
|
||||||
|
Get list of supported YANG models.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Capability objects
|
||||||
|
"""
|
||||||
|
caps = self.capabilities()
|
||||||
|
models = []
|
||||||
|
for model in caps.get("supported_models", []):
|
||||||
|
models.append(
|
||||||
|
Capability(
|
||||||
|
name=model.get("name", ""),
|
||||||
|
organization=model.get("organization", ""),
|
||||||
|
version=model.get("version", ""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return models
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
path: str | list[str],
|
||||||
|
data_type: DataType = "all",
|
||||||
|
encoding: str = "json_ietf",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get data at path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: YANG path or list of paths
|
||||||
|
data_type: Type of data to retrieve:
|
||||||
|
- "config": Configuration data only
|
||||||
|
- "state": State/operational data only
|
||||||
|
- "all": Both config and state (default)
|
||||||
|
encoding: Data encoding (default: json_ietf)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with path data
|
||||||
|
"""
|
||||||
|
client = self._ensure_connected()
|
||||||
|
paths = [path] if isinstance(path, str) else path
|
||||||
|
|
||||||
|
# Map data_type to pygnmi datatype parameter
|
||||||
|
datatype_map = {
|
||||||
|
"config": "config",
|
||||||
|
"state": "state",
|
||||||
|
"all": "all",
|
||||||
|
}
|
||||||
|
datatype = datatype_map.get(data_type, "all")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = client.get(path=paths, datatype=datatype, encoding=encoding)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e).lower()
|
||||||
|
if "not found" in error_msg or "invalid" in error_msg:
|
||||||
|
raise GNMIPathError(f"Path not found or invalid: {path}") from e
|
||||||
|
raise GNMIError(f"Failed to get data at {path}: {e}") from e
|
||||||
|
|
||||||
|
def set(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
value: Any,
|
||||||
|
operation: SetOperation = "update",
|
||||||
|
encoding: str = "json_ietf",
|
||||||
|
dry_run: bool = True,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Set configuration at path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: YANG path
|
||||||
|
value: Value to set (dict or JSON string)
|
||||||
|
operation: Set operation type:
|
||||||
|
- "update": Merge with existing config
|
||||||
|
- "replace": Replace existing config
|
||||||
|
- "delete": Delete config at path
|
||||||
|
encoding: Data encoding (default: json_ietf)
|
||||||
|
dry_run: If True, only validate without applying (default: True)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result of set operation
|
||||||
|
"""
|
||||||
|
if dry_run:
|
||||||
|
# For dry-run, just return what would be set
|
||||||
|
return {
|
||||||
|
"dry_run": True,
|
||||||
|
"operation": operation,
|
||||||
|
"path": path,
|
||||||
|
"value": value,
|
||||||
|
"message": "Dry-run mode - no changes applied",
|
||||||
|
}
|
||||||
|
|
||||||
|
client = self._ensure_connected()
|
||||||
|
|
||||||
|
# Parse value if it's a JSON string
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass # Keep as string - not valid JSON, will be set as string value
|
||||||
|
|
||||||
|
try:
|
||||||
|
if operation == "delete":
|
||||||
|
result = client.set(delete=[path])
|
||||||
|
elif operation == "replace":
|
||||||
|
result = client.set(replace=[(path, value)])
|
||||||
|
else: # update
|
||||||
|
result = client.set(update=[(path, value)])
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
raise GNMIError(f"Failed to set {path}: {e}") from e
|
||||||
|
|
||||||
|
def subscribe(
|
||||||
|
self,
|
||||||
|
paths: str | list[str],
|
||||||
|
mode: SubscribeMode = "stream",
|
||||||
|
stream_mode: StreamMode = "on-change",
|
||||||
|
sample_interval: int = 10,
|
||||||
|
encoding: str = "json_ietf",
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Subscribe to path updates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths: YANG path(s) to subscribe to
|
||||||
|
mode: Subscription mode:
|
||||||
|
- "once": Get data once and close
|
||||||
|
- "stream": Continuous updates
|
||||||
|
- "poll": Poll on demand
|
||||||
|
stream_mode: Stream mode (for mode="stream"):
|
||||||
|
- "on-change": Update on value change
|
||||||
|
- "sample": Periodic sampling
|
||||||
|
- "target-defined": Device decides
|
||||||
|
sample_interval: Interval in seconds for sample mode
|
||||||
|
encoding: Data encoding
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Subscription updates
|
||||||
|
"""
|
||||||
|
client = self._ensure_connected()
|
||||||
|
path_list = [paths] if isinstance(paths, str) else paths
|
||||||
|
|
||||||
|
# Build subscription list
|
||||||
|
subscribe_list = []
|
||||||
|
for p in path_list:
|
||||||
|
sub = {
|
||||||
|
"path": p,
|
||||||
|
"mode": stream_mode.replace("-", "_"), # on-change -> on_change
|
||||||
|
}
|
||||||
|
if stream_mode == "sample":
|
||||||
|
sub["sample_interval"] = sample_interval * 1_000_000_000 # nanoseconds
|
||||||
|
subscribe_list.append(sub)
|
||||||
|
|
||||||
|
subscribe_request = {
|
||||||
|
"subscription": subscribe_list,
|
||||||
|
"mode": mode,
|
||||||
|
"encoding": encoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return client.subscribe2(subscribe=subscribe_request)
|
||||||
|
except Exception as e:
|
||||||
|
raise GNMIError(f"Failed to subscribe to {paths}: {e}") from e
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Utility Methods
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def explore(self, path: str = "/", depth: int | None = None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Explore available paths under given path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Base path to explore (default: root)
|
||||||
|
depth: Maximum depth to explore (None = unlimited)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with discovered paths and data
|
||||||
|
"""
|
||||||
|
result = self.get(path, data_type="all")
|
||||||
|
|
||||||
|
if depth is not None and depth > 0:
|
||||||
|
# Limit depth by filtering result
|
||||||
|
# This is a simplified implementation
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def validate_path(self, path: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if path exists on device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: YANG path to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if path exists, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.get(path)
|
||||||
|
return True
|
||||||
|
except GNMIPathError:
|
||||||
|
return False
|
||||||
|
except GNMIError:
|
||||||
|
return False
|
||||||
@@ -6,18 +6,18 @@ managing Arista EVPN-VXLAN fabrics via gNMI.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .paths import (
|
from .paths import (
|
||||||
|
BGP,
|
||||||
|
EVPN,
|
||||||
|
MLAG,
|
||||||
|
VXLAN,
|
||||||
|
AfiSafi,
|
||||||
|
FabricSubscriptions,
|
||||||
Interfaces,
|
Interfaces,
|
||||||
Loopbacks,
|
Loopbacks,
|
||||||
VLANs,
|
|
||||||
BGP,
|
|
||||||
AfiSafi,
|
|
||||||
VXLAN,
|
|
||||||
MLAG,
|
|
||||||
EVPN,
|
|
||||||
PortChannel,
|
PortChannel,
|
||||||
System,
|
|
||||||
SubscriptionPath,
|
SubscriptionPath,
|
||||||
FabricSubscriptions,
|
System,
|
||||||
|
VLANs,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ Usage:
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Interfaces (OpenConfig)
|
# Interfaces (OpenConfig)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
321
uv.lock
generated
321
uv.lock
generated
@@ -3,34 +3,327 @@ revision = 3
|
|||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dataclasses"
|
name = "cffi"
|
||||||
version = "0.8"
|
version = "2.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/12/7919c5d8b9c497f9180db15ea8ead6499812ea8264a6ae18766d93c59fe5/dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97", size = 36581, upload-time = "2020-11-13T14:40:30.139Z" }
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf", size = 19041, upload-time = "2020-11-13T14:40:29.194Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "46.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dictdiffer"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/61/7b/35cbccb7effc5d7e40f4c55e2b79399e1853041997fcda15c9ff160abba0/dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578", size = 31513, upload-time = "2021-07-22T13:24:29.276Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fabric-orchestrator"
|
name = "fabric-orchestrator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "dataclasses" },
|
{ name = "click" },
|
||||||
{ name = "typing" },
|
{ name = "pygnmi" },
|
||||||
|
{ name = "rich" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "dataclasses", specifier = ">=0.8" },
|
{ name = "click", specifier = ">=8.1.0" },
|
||||||
{ name = "typing", specifier = ">=3.10.0.0" },
|
{ name = "pygnmi", specifier = ">=0.8.0" },
|
||||||
|
{ name = "rich", specifier = ">=13.0.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "ruff", specifier = ">=0.14.10" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "grpcio"
|
||||||
|
version = "1.76.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing"
|
name = "markdown-it-py"
|
||||||
version = "3.10.0.0"
|
version = "4.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/1b/835d4431805939d2996f8772aca1d2313a57e8860fec0e48e8e7dfe3a477/typing-3.10.0.0.tar.gz", hash = "sha256:13b4ad211f54ddbf93e5901a9967b1e07720c1d1b78d596ac6a439641aa1b130", size = 78962, upload-time = "2021-05-01T18:03:58.186Z" }
|
dependencies = [
|
||||||
wheels = [
|
{ name = "mdurl" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/5d/865e17349564eb1772688d8afc5e3081a5964c640d64d1d2880ebaed002d/typing-3.10.0.0-py3-none-any.whl", hash = "sha256:12fbdfbe7d6cca1a42e485229afcb0b0c8259258cfb919b8a5e2a5c953742f89", size = 26320, upload-time = "2021-05-01T18:03:56.398Z" },
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "6.33.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.23"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygnmi"
|
||||||
|
version = "0.8.15"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "dictdiffer" },
|
||||||
|
{ name = "grpcio" },
|
||||||
|
{ name = "protobuf" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/e8/aae8362da667452e5352875a7df700cc182740d14a01a88b22004bc318db/pygnmi-0.8.15.tar.gz", hash = "sha256:9b43d6334ac7ea73c0b8da395306b7618f4630adab96aaa2f28a79b3b52fdf58", size = 42938, upload-time = "2025-03-10T22:15:34.606Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/d8/1958e627d66f5f0c8dbdb1962850bc9e256fc65129ceb0212c0cb3e0d121/pygnmi-0.8.15-py3-none-any.whl", hash = "sha256:e72859070c7dc3618b578e48c76521db6d627b64a92b496ab886f3ff93a1a396", size = 35561, upload-time = "2025-03-10T22:15:32.981Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "14.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.14.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user