routers update
This commit is contained in:
parent
39c6843a5c
commit
ab6ac75391
11 changed files with 102 additions and 210 deletions
196
CLAUDE.md
196
CLAUDE.md
|
|
@ -4,163 +4,75 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a network documentation and discovery project that integrates with NetBox.
|
A small fleet of CLIs for documenting, auditing, and operating the vntx WISP — a multi-tower MikroTik backbone with cnMaestro and UISP managing the radios/CPE behind it. Source of truth for network documentation is NetBox at `https://netbox.vntx.net/` (API token in env var `NETBOX_KEY`); source of truth for the routers is whatever the live `.rsc` exports say.
|
||||||
|
|
||||||
## NetBox Integration
|
## Repo Layout
|
||||||
|
|
||||||
- **NetBox URL**: https://netbox.vntx.net/
|
The repo is a collection of independent CLIs that each operate on the same physical fleet but talk to different control planes. Each subproject has its own README/CLAUDE.md with details; only repo-wide context lives here.
|
||||||
- **API Key**: e50298f7fd20f7fd6f1931f635511b34f6e8cfde
|
|
||||||
- **Purpose**: Network documentation and discovery
|
|
||||||
|
|
||||||
## Development Guidelines
|
| Subdir | Lang | Talks to | Purpose |
|
||||||
|
|--|--|--|--|
|
||||||
|
| `mikrotik-tool/` | Go | MikroTik API-SSL + SSH | Fan-out to every router in `routers.yaml`: `list`, `export` (saves `<name>.rsc` per router), `inventory` (rewrites `inventory.yaml` from live data + SNMP), `api`/`schema` (ad-hoc API introspection on one router), `script` (push a `.rsc` file via SSH and stream output). Authoritative `inventory.yaml`, `subnets.yaml`, `radios.yaml`, `ipv6.md`, and `mpls.md` live here. |
|
||||||
|
| `mikrotik-tool-rs/` | Rust | MikroTik API-SSL + system `ssh` | Newer rewrite of a subset of `mikrotik-tool` (`list`, `export`, `run <cmd…>`) with `-r name1,name2` filter to scope a fan-out command. Has its own `routers.yaml`. See `mikrotik-tool-rs/CLAUDE.md`. |
|
||||||
|
| `uisp/` | Go (`main.go`) + Python (`uisp.py`) | UISP API at `uisp.vntx.net` | List/approve/upgrade UISP-managed devices. Auth via `UISP_KEY` env var. UNKNOWN-model devices are filtered by default (use `--include-unknown`). |
|
||||||
|
| `cnmaestro/` | Python (uv) | cnMaestro Cloud OAuth2 | Cleanup offline devices and bulk-upgrade firmware. `uv sync` + `.env` with client id/secret. See `cnmaestro/README.md`. |
|
||||||
|
|
||||||
### NetBox API Usage
|
`routers.yaml` at the repo root is the config consumed by `mikrotik-tool/` (Go); `mikrotik-tool-rs/` has its own copy. Keep them aligned when adding/removing routers.
|
||||||
- Use the provided API key for authentication with NetBox
|
|
||||||
- The NetBox instance is located at https://netbox.vntx.net/
|
|
||||||
- Follow NetBox API documentation for proper endpoint usage
|
|
||||||
- The netbox api token is in env var NETBOX_KEY
|
|
||||||
|
|
||||||
### Security Notes
|
`poll_snmp_site.sh` is a one-off helper for SNMP-polling a list of IPs at a site (`./poll_snmp_site.sh <site> <ip>...`). The `*.rsc` files at the repo root are stale exports — fresh ones come out of `mikrotik-tool` or `mikrotik-tool-rs`.
|
||||||
- Never commit API keys directly in code files
|
|
||||||
- Use environment variables or configuration files for sensitive data
|
|
||||||
- The API key provided should be stored securely
|
|
||||||
|
|
||||||
## Common Tasks
|
## Common Commands
|
||||||
|
|
||||||
### NetBox API Connection
|
|
||||||
When connecting to NetBox, use:
|
|
||||||
- Base URL: `https://netbox.vntx.net/api/`
|
|
||||||
- Authentication header: `Authorization: Token e50298f7fd20f7fd6f1931f635511b34f6e8cfde`
|
|
||||||
|
|
||||||
### MikroTik Router Connection
|
|
||||||
Connect to MikroTik routers using API-SSL:
|
|
||||||
- **Protocol**: API-SSL (port 8729)
|
|
||||||
- **Username**: `grahamro` (read-only)
|
|
||||||
- **Password**: `cFKhz8q5gPLoucMbcT1Iy58r3IXgc3`
|
|
||||||
- **Example - Verona Router**: `10.254.254.101`
|
|
||||||
|
|
||||||
Use the `mikrotik_connect.py` script to connect and retrieve router information:
|
|
||||||
```bash
|
```bash
|
||||||
python3 mikrotik_connect.py
|
# mikrotik-tool (Go)
|
||||||
|
cd mikrotik-tool && go build .
|
||||||
|
./mikrotik-tool list
|
||||||
|
./mikrotik-tool export # writes <name>.rsc per router into cwd
|
||||||
|
./mikrotik-tool api verona /ip/route/print
|
||||||
|
./mikrotik-tool schema verona /routing/bgp/connection
|
||||||
|
./mikrotik-tool script 494 494-v6-apply.rsc
|
||||||
|
./mikrotik-tool inventory # re-renders inventory.yaml in place
|
||||||
|
go test ./... # inventory_test.go
|
||||||
|
|
||||||
|
# mikrotik-tool-rs (Rust)
|
||||||
|
cd mikrotik-tool-rs && cargo build --release
|
||||||
|
cargo run -- list
|
||||||
|
cargo run -- -r verona,982 export # filter to a subset before the verb
|
||||||
|
cargo run -- run /system identity print
|
||||||
|
cargo test # full suite
|
||||||
|
cargo test encode_length # single test by name substring
|
||||||
|
cargo clippy --all-targets && cargo fmt
|
||||||
|
|
||||||
|
# uisp (Go binary, also a Python script)
|
||||||
|
cd uisp && go build .
|
||||||
|
./uisp list
|
||||||
|
./uisp approve <device>
|
||||||
|
./uisp upgrade
|
||||||
|
# python equivalent:
|
||||||
|
python3 uisp.py list
|
||||||
|
|
||||||
|
# cnmaestro (Python, uv)
|
||||||
|
cd cnmaestro && uv sync
|
||||||
|
uv run cnmaestro cleanup --dry-run
|
||||||
|
uv run cnmaestro upgrade --product ePMP,PMP --dry-run
|
||||||
|
uv run pytest
|
||||||
|
uv run pytest tests/test_devices.py::test_specific_thing # single test
|
||||||
```
|
```
|
||||||
|
|
||||||
The script handles SSL connection, authentication, and can retrieve:
|
`UISP_KEY` and `NETBOX_KEY` need to be exported in the shell. `cnmaestro` reads `cnmaestro/.env`.
|
||||||
- IP addresses and subnets
|
|
||||||
- Interface configurations
|
|
||||||
- Routing tables
|
|
||||||
- PPPoE connections
|
|
||||||
|
|
||||||
## Router Access Credentials
|
## NetBox
|
||||||
|
|
||||||
### MikroTik Routers
|
- URL: `https://netbox.vntx.net/`, API base `https://netbox.vntx.net/api/`
|
||||||
- Read-only access via API-SSL: username `grahamro`, password `cFKhz8q5gPLoucMbcT1Iy58r3IXgc3`
|
- Auth: `Authorization: Token $NETBOX_KEY`
|
||||||
|
- Convention when creating sites/devices: site slugs are lowercase-hyphenated, status `active`; routers are Manufacturer=MikroTik / Role=Router / primary IP = loopback `/32` (`10.254.254.x/32`).
|
||||||
|
- Prefix roles in use: Infrastructure, Customer, Management, Loopback.
|
||||||
|
|
||||||
### Verona Routers
|
## MikroTik Router Access
|
||||||
- Verona router is 10.254.254.101
|
|
||||||
|
|
||||||
### Additional Router IP Addresses
|
Read-only fan-out (used by all the above tools): API-SSL on port 8729, user `grahamro` / password in `CLAUDE.md` history. The committed `routers.yaml` actually uses `graham` (not `grahamro`) because `/export` requires the `ftp` policy in addition to `read,api`, which the read-only group lacks. New routers: ensure the user's group has `read,api,ftp` (or extend the read group: `/user group set read add-policy=ftp`).
|
||||||
- Climax router: 10.254.254.102
|
|
||||||
- Culleoka router: 10.254.254.104
|
|
||||||
|
|
||||||
## NetBox Site and Device Creation Process
|
Every router's loopback is `10.254.254.x/32`; see the **Fleet Topology** table below for the full mapping.
|
||||||
|
|
||||||
### Creating a new site and router in NetBox:
|
|
||||||
1. **Create Site**: Use `create_verona_site_and_router.py` as template
|
|
||||||
- Site name and slug (lowercase, hyphenated)
|
|
||||||
- Status: 'active'
|
|
||||||
- Comments describing the site
|
|
||||||
|
|
||||||
2. **Create Device**:
|
|
||||||
- Manufacturer: MikroTik
|
|
||||||
- Device Type: RouterBOARD
|
|
||||||
- Device Role: Router
|
|
||||||
- Primary IP: Router's loopback IP (e.g., 10.254.254.101/32)
|
|
||||||
|
|
||||||
3. **Add Network Data**: Use `update_netbox_verona.py` as template
|
|
||||||
- Creates prefixes with proper roles (Infrastructure, Customer, Management, Loopback)
|
|
||||||
- Creates interfaces on the device
|
|
||||||
- Associates IP addresses with interfaces
|
|
||||||
|
|
||||||
### API Authentication
|
|
||||||
- Always use environment variable `NETBOX_KEY` for API token
|
|
||||||
- Fallback to hardcoded token only if env var not set
|
|
||||||
|
|
||||||
## Generic Scripts for Network Management
|
|
||||||
|
|
||||||
### 1. Create Site Only in NetBox
|
|
||||||
```bash
|
|
||||||
# Basic usage
|
|
||||||
python3 create_site_only.py <site_name>
|
|
||||||
|
|
||||||
# With options
|
|
||||||
python3 create_site_only.py 380 --comments "Central site with multiple routers" --address "380 Main St"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create Site and Router in NetBox
|
|
||||||
```bash
|
|
||||||
# Basic usage
|
|
||||||
python3 create_site_and_router.py <site_name> <router_ip>
|
|
||||||
|
|
||||||
# With options
|
|
||||||
python3 create_site_and_router.py Climax 10.254.254.102 --router-name climax-core --physical-address "123 Tower Rd"
|
|
||||||
|
|
||||||
# For sites with multiple routers, create site first, then add each router
|
|
||||||
python3 create_site_only.py 380 --comments "Multi-router site"
|
|
||||||
python3 create_site_and_router.py 380 10.254.254.105 --router-name 380-core-router
|
|
||||||
python3 create_site_and_router.py 380 10.254.254.106 --router-name 380-edge-router
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Get MikroTik Router Configuration
|
|
||||||
```bash
|
|
||||||
# Basic usage (uses default read-only credentials)
|
|
||||||
python3 get_mikrotik_router_data.py <router_ip>
|
|
||||||
|
|
||||||
# Save to specific file
|
|
||||||
python3 get_mikrotik_router_data.py 10.254.254.102 -o climax_config.json
|
|
||||||
|
|
||||||
# Output JSON to stdout
|
|
||||||
python3 get_mikrotik_router_data.py 10.254.254.102 --json
|
|
||||||
|
|
||||||
# Custom credentials
|
|
||||||
python3 get_mikrotik_router_data.py 10.254.254.102 -u admin -p secretpass
|
|
||||||
```
|
|
||||||
|
|
||||||
The script retrieves:
|
|
||||||
- Router identity
|
|
||||||
- IP addresses and subnets
|
|
||||||
- Active interfaces
|
|
||||||
- VLANs
|
|
||||||
- PPPoE servers
|
|
||||||
- Static routes
|
|
||||||
|
|
||||||
### 4. Get MikroTik Router Data (Basic/Older RouterOS)
|
|
||||||
For older RouterOS versions or routers with many IPs (like CGNAT):
|
|
||||||
```bash
|
|
||||||
python3 get_mikrotik_basic_data.py <router_ip> -o router_data.json
|
|
||||||
```
|
|
||||||
|
|
||||||
This simplified script:
|
|
||||||
- Groups CGNAT addresses into a single subnet entry
|
|
||||||
- Focuses on key interfaces only
|
|
||||||
- Works better with older RouterOS versions
|
|
||||||
- Handles large configurations without timing out
|
|
||||||
|
|
||||||
### 5. Get All Network Devices
|
|
||||||
```bash
|
|
||||||
# Get all devices categorized by type
|
|
||||||
python3 get_all_network_devices.py <router_ip>
|
|
||||||
|
|
||||||
# Show all devices including "Other" category
|
|
||||||
python3 get_all_network_devices.py <router_ip> --show-all
|
|
||||||
|
|
||||||
# Save to JSON file
|
|
||||||
python3 get_all_network_devices.py <router_ip> -o devices.json
|
|
||||||
```
|
|
||||||
|
|
||||||
This script retrieves DHCP leases and ARP table to identify:
|
|
||||||
- Ubiquiti access points and devices
|
|
||||||
- MikroTik devices
|
|
||||||
- Other network devices
|
|
||||||
|
|
||||||
## Network Topology Patterns
|
## Network Topology Patterns
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.21.4
|
# 2026-05-09 21:18:17 by RouterOS 7.21.4
|
||||||
# software id = K4QG-8NQV
|
# software id = K4QG-8NQV
|
||||||
#
|
#
|
||||||
# model = RB5009UG+S+
|
# model = RB5009UG+S+
|
||||||
|
|
@ -378,19 +378,15 @@ add bridge=management interface=vlan10_ether7 internal-path-cost=10 \
|
||||||
add bridge=management interface=vlan10_ether8 internal-path-cost=10 \
|
add bridge=management interface=vlan10_ether8 internal-path-cost=10 \
|
||||||
path-cost=10
|
path-cost=10
|
||||||
/ip firewall connection tracking
|
/ip firewall connection tracking
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set icmp-timeout=30s tcp-close-wait-timeout=1m tcp-established-timeout=4h \
|
set icmp-timeout=30s tcp-close-wait-timeout=1m tcp-established-timeout=4h \
|
||||||
tcp-fin-wait-timeout=2m tcp-last-ack-timeout=30s \
|
tcp-fin-wait-timeout=2m tcp-last-ack-timeout=30s \
|
||||||
tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
||||||
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
||||||
/ip neighbor discovery-settings
|
/ip neighbor discovery-settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set discover-interface-list=!dynamic
|
set discover-interface-list=!dynamic
|
||||||
/ip settings
|
/ip settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set tcp-syncookies=yes
|
set tcp-syncookies=yes
|
||||||
/ipv6 settings
|
/ipv6 settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set accept-router-advertisements=no
|
set accept-router-advertisements=no
|
||||||
/interface pppoe-server server
|
/interface pppoe-server server
|
||||||
add default-profile=494 disabled=no interface=494 max-mru=1492 max-mtu=1492 \
|
add default-profile=494 disabled=no interface=494 max-mru=1492 max-mtu=1492 \
|
||||||
|
|
@ -543,8 +539,6 @@ add address=10.250.1.70%ether2-climax area=ospf-area-1 disabled=no \
|
||||||
poll-interval=10s
|
poll-interval=10s
|
||||||
/snmp
|
/snmp
|
||||||
set contact=graham@vntx.net enabled=yes location="33.208204, -96.462530"
|
set contact=graham@vntx.net enabled=yes location="33.208204, -96.462530"
|
||||||
/system clock
|
|
||||||
set time-zone-name=America/Chicago
|
|
||||||
/system identity
|
/system identity
|
||||||
set name=494
|
set name=494
|
||||||
/system logging
|
/system logging
|
||||||
|
|
@ -560,7 +554,6 @@ add address=0.us.pool.ntp.org
|
||||||
/system package update
|
/system package update
|
||||||
set channel=long-term
|
set channel=long-term
|
||||||
/system routerboard settings
|
/system routerboard settings
|
||||||
# Firmware upgraded successfully, please reboot for changes to take effect!
|
|
||||||
set auto-upgrade=yes
|
set auto-upgrade=yes
|
||||||
/system scheduler
|
/system scheduler
|
||||||
add name=reboot on-event="/system reboot" policy=\
|
add name=reboot on-event="/system reboot" policy=\
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.21.4
|
# 2026-05-10 14:48:48 by RouterOS 7.21.4
|
||||||
# software id = FUVS-HCM5
|
# software id = FUVS-HCM5
|
||||||
#
|
#
|
||||||
# model = CCR1009-7G-1C-1S+
|
# model = CCR1009-7G-1C-1S+
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:04 by RouterOS 7.21.4
|
# 2026-05-09 16:18:38 by RouterOS 7.21.4
|
||||||
# software id = UETF-WF31
|
# software id = UETF-WF31
|
||||||
#
|
#
|
||||||
# model = CCR2004-16G-2S+
|
# model = CCR2004-16G-2S+
|
||||||
|
|
@ -299,19 +299,15 @@ add bridge=climax-bridge interface=sfp-sfpplus1
|
||||||
add bridge=mgmt interface=vlan10_ether7
|
add bridge=mgmt interface=vlan10_ether7
|
||||||
add bridge=mgmt interface=vlan10_sfp-sfpplus1
|
add bridge=mgmt interface=vlan10_sfp-sfpplus1
|
||||||
/ip firewall connection tracking
|
/ip firewall connection tracking
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set enabled=yes icmp-timeout=30s tcp-close-wait-timeout=1m \
|
set enabled=yes icmp-timeout=30s tcp-close-wait-timeout=1m \
|
||||||
tcp-established-timeout=4h tcp-fin-wait-timeout=2m tcp-last-ack-timeout=\
|
tcp-established-timeout=4h tcp-fin-wait-timeout=2m tcp-last-ack-timeout=\
|
||||||
30s tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
30s tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
||||||
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
||||||
/ip neighbor discovery-settings
|
/ip neighbor discovery-settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set discover-interface-list=!dynamic
|
set discover-interface-list=!dynamic
|
||||||
/ip settings
|
/ip settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set max-neighbor-entries=8192 tcp-syncookies=yes
|
set max-neighbor-entries=8192 tcp-syncookies=yes
|
||||||
/ipv6 settings
|
/ipv6 settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set accept-router-advertisements=no
|
set accept-router-advertisements=no
|
||||||
/interface pppoe-server server
|
/interface pppoe-server server
|
||||||
add authentication=chap,mschap2 default-profile=pppoe disabled=no interface=\
|
add authentication=chap,mschap2 default-profile=pppoe disabled=no interface=\
|
||||||
|
|
@ -528,5 +524,4 @@ add address=0.us.pool.ntp.org
|
||||||
/system package update
|
/system package update
|
||||||
set channel=long-term
|
set channel=long-term
|
||||||
/system routerboard settings
|
/system routerboard settings
|
||||||
# Firmware upgraded successfully, please reboot for changes to take effect!
|
|
||||||
set auto-upgrade=yes enter-setup-on=delete-key
|
set auto-upgrade=yes enter-setup-on=delete-key
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.21.4
|
# 2026-05-09 16:20:09 by RouterOS 7.21.4
|
||||||
# software id = XS5B-41QR
|
# software id = XS5B-41QR
|
||||||
#
|
#
|
||||||
# model = CCR1009-7G-1C-1S+
|
# model = CCR1009-7G-1C-1S+
|
||||||
|
|
@ -372,17 +372,13 @@ add name=logs remote=204.110.191.229 src-address=10.254.254.253 target=remote
|
||||||
/ip smb
|
/ip smb
|
||||||
set enabled=no
|
set enabled=no
|
||||||
/ip firewall connection tracking
|
/ip firewall connection tracking
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set tcp-established-timeout=4h tcp-fin-wait-timeout=2m tcp-time-wait-timeout=\
|
set tcp-established-timeout=4h tcp-fin-wait-timeout=2m tcp-time-wait-timeout=\
|
||||||
2m
|
2m
|
||||||
/ip neighbor discovery-settings
|
/ip neighbor discovery-settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set discover-interface-list=!dynamic
|
set discover-interface-list=!dynamic
|
||||||
/ip settings
|
/ip settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set max-neighbor-entries=8192 tcp-syncookies=yes
|
set max-neighbor-entries=8192 tcp-syncookies=yes
|
||||||
/ipv6 settings
|
/ipv6 settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set accept-router-advertisements=no max-neighbor-entries=8192 \
|
set accept-router-advertisements=no max-neighbor-entries=8192 \
|
||||||
soft-max-neighbor-entries=8191
|
soft-max-neighbor-entries=8191
|
||||||
/interface pppoe-server server
|
/interface pppoe-server server
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.21.4
|
# 2026-05-09 16:21:31 by RouterOS 7.21.4
|
||||||
# software id = HVP9-3G0K
|
# software id = HVP9-3G0K
|
||||||
#
|
#
|
||||||
# model = CCR1009-7G-1C-1S+
|
# model = CCR1009-7G-1C-1S+
|
||||||
|
|
@ -375,16 +375,13 @@ add bridge=culleoka disabled=yes ingress-filtering=no interface=*10 \
|
||||||
add bridge=mgmt ingress-filtering=no interface=vlan10_ether2 \
|
add bridge=mgmt ingress-filtering=no interface=vlan10_ether2 \
|
||||||
internal-path-cost=10 path-cost=10
|
internal-path-cost=10 path-cost=10
|
||||||
/ip firewall connection tracking
|
/ip firewall connection tracking
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set icmp-timeout=30s tcp-close-wait-timeout=1m tcp-established-timeout=4h \
|
set icmp-timeout=30s tcp-close-wait-timeout=1m tcp-established-timeout=4h \
|
||||||
tcp-fin-wait-timeout=2m tcp-last-ack-timeout=30s \
|
tcp-fin-wait-timeout=2m tcp-last-ack-timeout=30s \
|
||||||
tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
||||||
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
||||||
/ip settings
|
/ip settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set tcp-syncookies=yes
|
set tcp-syncookies=yes
|
||||||
/ipv6 settings
|
/ipv6 settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set accept-router-advertisements=no
|
set accept-router-advertisements=no
|
||||||
/interface pppoe-server server
|
/interface pppoe-server server
|
||||||
add accept-empty-service=no authentication=chap,mschap2 default-profile=pppoe \
|
add accept-empty-service=no authentication=chap,mschap2 default-profile=pppoe \
|
||||||
|
|
@ -725,7 +722,6 @@ add address=0.us.pool.ntp.org
|
||||||
/system package update
|
/system package update
|
||||||
set channel=long-term
|
set channel=long-term
|
||||||
/system routerboard settings
|
/system routerboard settings
|
||||||
# Firmware upgraded successfully, please reboot for changes to take effect!
|
|
||||||
set auto-upgrade=yes
|
set auto-upgrade=yes
|
||||||
/system scheduler
|
/system scheduler
|
||||||
add interval=5m name=remove_hotspot on-event=\
|
add interval=5m name=remove_hotspot on-event=\
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# may/09/2026 15:08:04 by RouterOS 6.49.18
|
# may/10/2026 05:53:37 by RouterOS 6.49.18
|
||||||
# software id = 8XZE-R7EJ
|
# software id = 8XZE-R7EJ
|
||||||
#
|
#
|
||||||
# model = CCR2004-1G-12S+2XS
|
# model = CCR2004-1G-12S+2XS
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.22.3
|
# 2026-05-10 14:48:48 by RouterOS 7.22.3
|
||||||
# software id = ZGNY-ZJW7
|
# software id = ZGNY-ZJW7
|
||||||
#
|
#
|
||||||
# model = RB5009UG+S+
|
# model = RB5009UG+S+
|
||||||
|
|
@ -100,7 +100,6 @@ add bridge=bridge comment=defconf ingress-filtering=no interface=sfp-sfpplus1 \
|
||||||
internal-path-cost=10 path-cost=10
|
internal-path-cost=10 path-cost=10
|
||||||
add bridge=dockers interface=veth1
|
add bridge=dockers interface=veth1
|
||||||
/ipv6 settings
|
/ipv6 settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set accept-router-advertisements=yes
|
set accept-router-advertisements=yes
|
||||||
/interface detect-internet
|
/interface detect-internet
|
||||||
set detect-interface-list=all internet-interface-list=all lan-interface-list=\
|
set detect-interface-list=all internet-interface-list=all lan-interface-list=\
|
||||||
|
|
@ -430,7 +429,7 @@ add comment=wigle.net disabled=no distance=1 dst-address=54.70.85.50/32 \
|
||||||
add dst-address=100.64.0.0/10 gateway=172.17.0.2
|
add dst-address=100.64.0.0/10 gateway=172.17.0.2
|
||||||
add comment="Default via TMO (primary)" disabled=no distance=1 dst-address=\
|
add comment="Default via TMO (primary)" disabled=no distance=1 dst-address=\
|
||||||
0.0.0.0/0 gateway=192.168.12.1 target-scope=11
|
0.0.0.0/0 gateway=192.168.12.1 target-scope=11
|
||||||
add comment="Default via VNTX (secondary)" disabled=no distance=2 \
|
add comment="Default via VNTX (secondary)" disabled=yes distance=2 \
|
||||||
dst-address=0.0.0.0/0 gateway=204.110.191.30 target-scope=11
|
dst-address=0.0.0.0/0 gateway=204.110.191.30 target-scope=11
|
||||||
add check-gateway=ping comment="Default via Starlink (last\
|
add check-gateway=ping comment="Default via Starlink (last\
|
||||||
\n resort)" distance=3 dst-address=0.0.0.0/0 gateway=4.2.2.3 \
|
\n resort)" distance=3 dst-address=0.0.0.0/0 gateway=4.2.2.3 \
|
||||||
|
|
@ -512,7 +511,6 @@ set enabled=yes
|
||||||
/system ntp client servers
|
/system ntp client servers
|
||||||
add address=ntp.vntx.net
|
add address=ntp.vntx.net
|
||||||
/system routerboard settings
|
/system routerboard settings
|
||||||
# Firmware upgraded successfully, please reboot for changes to take effect!
|
|
||||||
set auto-upgrade=yes
|
set auto-upgrade=yes
|
||||||
/tool e-mail
|
/tool e-mail
|
||||||
set certificate-verification=no from=mikrotik@vntx.net port=2525 server=\
|
set certificate-verification=no from=mikrotik@vntx.net port=2525 server=\
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.21.4
|
# 2026-05-10 14:48:49 by RouterOS 7.21.4
|
||||||
# software id = 5HTF-YFWV
|
# software id = 5HTF-YFWV
|
||||||
#
|
#
|
||||||
# model = CCR1009-7G-1C-1S+
|
# model = CCR1009-7G-1C-1S+
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 2026-05-09 15:08:05 by RouterOS 7.21.4
|
# 2026-05-09 16:19:30 by RouterOS 7.21.4
|
||||||
# software id = Y1CT-1WB1
|
# software id = Y1CT-1WB1
|
||||||
#
|
#
|
||||||
# model = CCR2004-16G-2S+
|
# model = CCR2004-16G-2S+
|
||||||
|
|
@ -456,19 +456,15 @@ add bridge=*1F interface=*23 internal-path-cost=10 path-cost=10
|
||||||
add bridge=cpe_vlan_10 interface=vlan10_ether15
|
add bridge=cpe_vlan_10 interface=vlan10_ether15
|
||||||
add bridge=cpe_vlan_10 interface=ether10-powerswitch
|
add bridge=cpe_vlan_10 interface=ether10-powerswitch
|
||||||
/ip firewall connection tracking
|
/ip firewall connection tracking
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set icmp-timeout=30s tcp-close-wait-timeout=1m tcp-established-timeout=4h \
|
set icmp-timeout=30s tcp-close-wait-timeout=1m tcp-established-timeout=4h \
|
||||||
tcp-fin-wait-timeout=2m tcp-last-ack-timeout=30s \
|
tcp-fin-wait-timeout=2m tcp-last-ack-timeout=30s \
|
||||||
tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
tcp-syn-received-timeout=1m tcp-syn-sent-timeout=2m \
|
||||||
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
tcp-time-wait-timeout=2m udp-stream-timeout=2m
|
||||||
/ip neighbor discovery-settings
|
/ip neighbor discovery-settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set discover-interface-list=!dynamic
|
set discover-interface-list=!dynamic
|
||||||
/ip settings
|
/ip settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set tcp-syncookies=yes
|
set tcp-syncookies=yes
|
||||||
/ipv6 settings
|
/ipv6 settings
|
||||||
# ipv6 *accept router advertisements* configuration has changed, please restart device to apply settings
|
|
||||||
set accept-router-advertisements=no
|
set accept-router-advertisements=no
|
||||||
/interface pppoe-server server
|
/interface pppoe-server server
|
||||||
add default-profile=pppoe-verona disabled=no interface=verona max-mru=1500 \
|
add default-profile=pppoe-verona disabled=no interface=verona max-mru=1500 \
|
||||||
|
|
@ -1025,7 +1021,6 @@ add address=0.us.pool.ntp.org
|
||||||
/system package update
|
/system package update
|
||||||
set channel=long-term
|
set channel=long-term
|
||||||
/system routerboard settings
|
/system routerboard settings
|
||||||
# Firmware upgraded successfully, please reboot for changes to take effect!
|
|
||||||
set auto-upgrade=yes enter-setup-on=delete-key
|
set auto-upgrade=yes enter-setup-on=delete-key
|
||||||
/system scheduler
|
/system scheduler
|
||||||
add name=reboot on-event="/system reboot" policy=\
|
add name=reboot on-event="/system reboot" policy=\
|
||||||
|
|
|
||||||
69
uisp/uisp.py
69
uisp/uisp.py
|
|
@ -134,6 +134,21 @@ def latest_firmware_for(fws: list[dict], d: dict) -> str:
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_in_flight(d: dict, target_v: str = "") -> bool:
|
||||||
|
"""True if UISP already has a pending/running upgrade for this device.
|
||||||
|
Re-POSTing /tasks for a device with one queued returns a misleading
|
||||||
|
422 "Firmware not found" error; skip these instead.
|
||||||
|
"""
|
||||||
|
up = d.get("upgrade") or {}
|
||||||
|
status = (up.get("status") or "").lower()
|
||||||
|
if status not in {"queued", "running", "in-progress", "started", "preparing", "downloading", "installing"}:
|
||||||
|
return False
|
||||||
|
if not target_v:
|
||||||
|
return True
|
||||||
|
# Only skip if the queued upgrade is for the same target we'd request.
|
||||||
|
return up.get("firmwareVersion") == target_v
|
||||||
|
|
||||||
|
|
||||||
def needs_upgrade(d: dict, fws: list[dict]) -> bool:
|
def needs_upgrade(d: dict, fws: list[dict]) -> bool:
|
||||||
ident = d.get("identification") or {}
|
ident = d.get("identification") or {}
|
||||||
if ident.get("type") == "blackBox":
|
if ident.get("type") == "blackBox":
|
||||||
|
|
@ -147,7 +162,11 @@ def needs_upgrade(d: dict, fws: list[dict]) -> bool:
|
||||||
latest = latest_firmware_for(fws, d)
|
latest = latest_firmware_for(fws, d)
|
||||||
if not latest:
|
if not latest:
|
||||||
return False
|
return False
|
||||||
return version_lt(cur, latest)
|
if not version_lt(cur, latest):
|
||||||
|
return False
|
||||||
|
if upgrade_in_flight(d, latest):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_unauthorized(d: dict) -> bool:
|
def is_unauthorized(d: dict) -> bool:
|
||||||
|
|
@ -595,7 +614,7 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
||||||
print(f"\ndry-run: pass --yes to actually trigger upgrades.")
|
print(f"\ndry-run: pass --yes to actually trigger upgrades.")
|
||||||
return
|
return
|
||||||
|
|
||||||
batch_size = max(1, args.batch_size)
|
batch_size = args.batch_size if args.batch_size > 0 else len(targets)
|
||||||
n_batches = (len(targets) + batch_size - 1) // batch_size
|
n_batches = (len(targets) + batch_size - 1) // batch_size
|
||||||
failed_total = 0
|
failed_total = 0
|
||||||
|
|
||||||
|
|
@ -603,45 +622,35 @@ def cmd_upgrade(args: argparse.Namespace) -> None:
|
||||||
batch = targets[batch_idx * batch_size:(batch_idx + 1) * batch_size]
|
batch = targets[batch_idx * batch_size:(batch_idx + 1) * batch_size]
|
||||||
print(f"\n=== batch {batch_idx + 1}/{n_batches}: {len(batch)} device(s) ===")
|
print(f"\n=== batch {batch_idx + 1}/{n_batches}: {len(batch)} device(s) ===")
|
||||||
|
|
||||||
# Build the upgrades payload and POST one task per device. UISP's
|
# POST /tasks one device at a time. Bulk submission (all device
|
||||||
# actual upgrade endpoint is POST /tasks with
|
# upgrades in a single /tasks payload) is atomic: any one device
|
||||||
# {upgrades: [{deviceId, firmwareVersion}], upgradeInMaintenanceWindow}.
|
# whose target firmwareVersion doesn't exactly match a catalog
|
||||||
# /devices/{id}/update only refreshes device data — it does NOT
|
# entry causes the whole call to 422 with "Firmware not found".
|
||||||
# trigger a firmware upgrade.
|
# Per-device /tasks isolates failures.
|
||||||
triggered: list[tuple[str, str, str]] = [] # (id, name, target_version)
|
triggered: list[tuple[str, str, str]] = [] # (id, name, target_version)
|
||||||
upgrades = []
|
|
||||||
for d in batch:
|
for d in batch:
|
||||||
did = d["identification"]["id"]
|
did = d["identification"]["id"]
|
||||||
target_v = latest_firmware_for(fws, d)
|
target_v = latest_firmware_for(fws, d)
|
||||||
if not target_v:
|
if not target_v:
|
||||||
print(f" skip {did} {disp_name(d)}: no catalog firmware match", file=sys.stderr)
|
print(f" skip {did} {disp_name(d):<30}: no catalog firmware match", file=sys.stderr)
|
||||||
failed_total += 1
|
failed_total += 1
|
||||||
continue
|
continue
|
||||||
upgrades.append({
|
if upgrade_in_flight(d, target_v):
|
||||||
"deviceId": did,
|
up = d.get("upgrade") or {}
|
||||||
"firmwareVersion": target_v,
|
print(f" skip {did} {disp_name(d):<30}: already {up.get('status')} → {up.get('firmwareVersion')}")
|
||||||
})
|
continue
|
||||||
triggered.append((did, disp_name(d), target_v))
|
|
||||||
|
|
||||||
if upgrades:
|
|
||||||
try:
|
try:
|
||||||
api("POST", "/tasks", {
|
api("POST", "/tasks", {
|
||||||
"upgrades": upgrades,
|
"upgrades": [{"deviceId": did, "firmwareVersion": target_v}],
|
||||||
"upgradeInMaintenanceWindow": False,
|
"upgradeInMaintenanceWindow": False,
|
||||||
})
|
})
|
||||||
for did, name, tv in triggered:
|
print(f" trigger OK {did} {disp_name(d):<30} → {target_v}")
|
||||||
print(f" trigger OK {did} {name:<30} → {tv}")
|
triggered.append((did, disp_name(d), target_v))
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
print(f" batch trigger FAILED for {len(upgrades)} device(s)", file=sys.stderr)
|
print(f" trigger FAIL {did} {disp_name(d):<30} → {target_v}", file=sys.stderr)
|
||||||
failed_total += len(upgrades)
|
failed_total += 1
|
||||||
triggered = []
|
|
||||||
|
|
||||||
if not args.wait:
|
if not args.wait:
|
||||||
# No completion wait — just space batches out so we don't blast
|
|
||||||
# the whole fleet simultaneously.
|
|
||||||
if batch_idx + 1 < n_batches:
|
|
||||||
print(f" sleeping {args.batch_delay}s before next batch...")
|
|
||||||
time.sleep(args.batch_delay)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Wait for every device in this batch to land on its target version.
|
# Wait for every device in this batch to land on its target version.
|
||||||
|
|
@ -724,14 +733,12 @@ def main() -> None:
|
||||||
pu.add_argument("--all", action="store_true", help="upgrade every device with a newer firmware available")
|
pu.add_argument("--all", action="store_true", help="upgrade every device with a newer firmware available")
|
||||||
pu.add_argument("--yes", action="store_true", help="actually trigger upgrades (default: dry-run)")
|
pu.add_argument("--yes", action="store_true", help="actually trigger upgrades (default: dry-run)")
|
||||||
pu.add_argument("--force", action="store_true", help="upgrade even if already on latest")
|
pu.add_argument("--force", action="store_true", help="upgrade even if already on latest")
|
||||||
pu.add_argument("--batch-size", type=int, default=5,
|
pu.add_argument("--batch-size", type=int, default=0,
|
||||||
help="how many devices to upgrade in parallel per batch (default: 5)")
|
help="devices per batch; 0 means all in one batch (default). Only useful with --wait.")
|
||||||
pu.add_argument("--wait", action="store_true",
|
pu.add_argument("--wait", action="store_true",
|
||||||
help="wait for each batch to land on the new firmware before starting the next")
|
help="wait for each batch to land on the new firmware before starting the next")
|
||||||
pu.add_argument("--wait-timeout", type=int, default=900,
|
pu.add_argument("--wait-timeout", type=int, default=900,
|
||||||
help="seconds to wait per batch when --wait is set (default: 900)")
|
help="seconds to wait per batch when --wait is set (default: 900)")
|
||||||
pu.add_argument("--batch-delay", type=int, default=30,
|
|
||||||
help="seconds to sleep between batches when --wait is NOT set (default: 30)")
|
|
||||||
pu.set_defaults(func=cmd_upgrade)
|
pu.set_defaults(func=cmd_upgrade)
|
||||||
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue