2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-07-18 00:16:50 +00:00
Files
confluent/misc/getipsfromswitchport
Jarrod Johnson 164a168694 Add fallback to mac table
If LLDP is uncooperative, maybe the mac was learned.

If no mac apparently learned, then we ping_everywhere in hopes of soliciting traffic, and then rescan the switches.

Then get all mac addresses, try to determine zone from generated mac, and print on success.
2026-07-15 08:24:33 -04:00

206 lines
6.5 KiB
Python

#!/usr/bin/python
import asyncio
import re
import sys
try:
import confluent.asynclient as asynclient
except ImportError:
sys.path.append('/opt/confluent/lib/python')
import confluent.asynclient as asynclient
_whitelistnames = (
# 3com
re.compile(r'^RMON Port (\d+) on unit \d+'),
# Dell
re.compile(r'^Unit \d+ Port (\d+)\Z'),
)
_blacklistnames = (
re.compile(r'vl'),
re.compile(r'Nu'),
re.compile(r'RMON'),
re.compile(r'onsole'),
re.compile(r'Stack'),
re.compile(r'Trunk'),
re.compile(r'po\d'),
re.compile(r'XGE'),
re.compile(r'LAG'),
re.compile(r'CPU'),
re.compile(r'Management'),
)
def mac2lla(mac):
"""Convert MAC address to IPv6 link-local address."""
# Remove colons from MAC address
mac_clean = mac.replace(':', '')
# Insert fe80:: prefix and convert to lowercase
# Split MAC into first 3 octets and last 3 octets
first_half = mac_clean[:6]
second_half = mac_clean[6:]
# Flip the universal/local bit in the first octet
first_octet = int(first_half[:2], 16)
first_octet ^= 0x02
first_half = f'{first_octet:02x}' + first_half[2:]
# Format as IPv6 link-local address
lla = f'fe80::{first_half[0:4]}:{first_half[4:]}ff:fe{second_half[0:2]}:{second_half[2:6]}'
return lla
async def is_reachable(address):
try:
await asyncio.wait_for(asyncio.open_connection(address, 22), timeout=0.5)
return True
except (asyncio.TimeoutError, OSError):
return False
async def get_nics():
proc = await asyncio.create_subprocess_exec(
'ip', '-json', 'link',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, _ = await proc.communicate()
import json
links = json.loads(stdout)
for link in links:
if 'LOOPBACK' in link.get('flags', []):
continue
if 'LOWER_UP' not in link.get('flags', []):
continue
yield link['ifname']
async def add_zone(lla):
if '%' in lla:
return lla
tocheck = []
async for link in get_nics():
tocheck.append(link)
tasks = []
for link in tocheck:
zone_addr = lla + '%' + link
tasks.append(is_reachable(zone_addr))
results = await asyncio.gather(*tasks)
for link, reachable in zip(tocheck, results):
if reachable:
return lla + '%' + link
return lla
def _namesmatch(switchdesc, userdesc):
if switchdesc is None:
return False
if switchdesc == userdesc:
return True
try:
portnum = int(userdesc)
except ValueError:
portnum = None
if portnum is not None:
for exp in _whitelistnames:
match = exp.match(switchdesc)
if match:
snum = int(match.groups()[0])
if snum == portnum:
return True
anymatch = re.search(r'[^0123456789]' + userdesc + r'(\.0)?\Z', switchdesc)
if anymatch:
for blexp in _blacklistnames:
if blexp.match(switchdesc):
return False
return True
return False
async def ping_everywhere():
tasks = []
async for link in get_nics():
tasks.append(asyncio.create_subprocess_exec(
'ping', '-I', link, 'ff02::1', '-c', '2',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
))
for task in tasks:
proc = await task
await proc.communicate()
async def main(switch, port):
portname = None
client = asynclient.Command()
async for rsp in client.read(f'/networking/neighbors/by-switch/{switch}/by-port/'):
portcandidate = rsp.get('item', {}).get('href')[:-1]
if _namesmatch(portcandidate, port):
if portname:
sys.stderr.write(f"Multiple matches found for port {port} on switch {switch}\n")
portname = None
else:
portname = portcandidate
peerid = None
if portname:
async for rsp in client.read(f'/networking/neighbors/by-switch/{switch}/by-port/{portname}/by-peerid/'):
peerid = rsp.get('item', {}).get('href')
fe80found = False
if peerid:
maybemac = None
async for rsp in client.read(f'/networking/neighbors/by-switch/{switch}/by-port/{portname}/by-peerid/{peerid}'):
if 'peeraddresses' in rsp:
for addr in rsp['peeraddresses']:
if addr.startswith('fe80::'):
fe80found = True
addr = await add_zone(addr)
print(addr)
if 'peerchassisid' in rsp:
maybemac = rsp['peerchassisid']
if not fe80found and maybemac:
try:
maybella = mac2lla(maybemac)
maybella = await add_zone(maybella)
fe80found = True
print(maybella)
except Exception as e:
pass
if not fe80found:
if not portname:
async for rsp in client.read(f'/networking/macs/by-switch/{switch}/by-port/'):
portcandidate = rsp.get('item', {}).get('href')[:-1]
if _namesmatch(portcandidate, port):
if portname:
sys.stderr.write(f"Multiple matches found for port {port} on switch {switch}\n")
portname = None
else:
portname = portcandidate
if not portname:
await ping_everywhere()
async for rsp in client.update(f'/networking/macs/rescan', {'rescan': 'start'}):
pass
scanning = True
while scanning:
async for rsp in client.read(f'/networking/macs/rescan'):
if 'scanning' in rsp:
scanning = rsp['scanning']
if scanning:
await asyncio.sleep(1)
async for rsp in client.read(f'/networking/macs/by-switch/{switch}/by-port/{portname}/by-mac/'):
mac = rsp.get('item', {}).get('href').replace('-', ':')
if mac:
try:
maybella = mac2lla(mac)
maybella = await add_zone(maybella)
fe80found = True
print(maybella)
except Exception as e:
pass
if __name__ == '__main__':
if len(sys.argv) < 3:
sys.stderr.write(f"Usage: {sys.argv[0]} <switch> <port>\n")
sys.exit(1)
switch = sys.argv[1]
port = sys.argv[2]
asyncio.run(main(switch, port))