import json,urllib.request,subprocess,re,collections import concurrent.futures as cf import l2cfg TOK='d3abb125eb301a021ef80a2847a803a7cd2d0b1d3c8337a14d09db9d571369b5' API='http://192.168.87.20:8080/api_jsonrpc.php' def call(m,p): d=json.dumps({"jsonrpc":"2.0","method":m,"params":p,"id":1}).encode() q=urllib.request.Request(API,data=d,headers={'Content-Type':'application/json','Authorization':'Bearer '+TOK}) return json.loads(urllib.request.urlopen(q,timeout=60).read().decode()) AUTH={0:'MD5',1:'SHA',2:'SHA-224',3:'SHA-256',4:'SHA-384',5:'SHA-512'};PRIV={0:'DES',1:'AES',2:'AES-192',3:'AES-256',4:'AES',5:'AES-256'} hs=l2cfg.hosts(call, output=['hostid','name']) def creds(i): det=i.get('details') or {};ver=str(det.get('version','1')) if ver=='3': lvl=int(det.get('securitylevel',0));a=['-v3','-l',{0:'noAuthNoPriv',1:'authNoPriv',2:'authPriv'}.get(lvl,'authPriv'),'-u',det.get('securityname','')] if lvl>=1:a+=['-a',AUTH.get(int(det.get('authprotocol',0)),'SHA'),'-A',det.get('authpassphrase','')] if lvl>=2:a+=['-x',PRIV.get(int(det.get('privprotocol',0)),'AES'),'-X',det.get('privpassphrase','')] return a return ['-v2c','-c',det.get('community','public')] tasks=[] for h in hs: for i in h['interfaces']: if i.get('type')=='2': tasks.append((i['ip'],h['name'],i));break def walk(args,ip,oid): try: r=subprocess.run(['snmpbulkwalk','-On']+args+['-t','3','-r','1',ip,oid],capture_output=True,text=True,timeout=40) return [l for l in r.stdout.splitlines() if ' = ' in l and 'No Such' not in l] except: return [] def num(v): m=re.search(r'-?\d+',v);return int(m.group()) if m else None def hexmac(v): # value like Hex-STRING: 80 00 6C 71 D2 10 F6 00 if ':' in v: v=v.split(':',1)[1] parts=re.findall(r'[0-9A-Fa-f]{2}',v) return [int(p,16) for p in parts] def mac(p): return ':'.join('%02x'%b for b in p) def strval(v): return v.split(':',1)[1].strip().strip('"') if ':' in v else v.strip() def parse(lines,base): out=[] for ln in lines: l,r=ln.split(' = ',1);o=l.strip().lstrip('.') if o.startswith(base): out.append((o[len(base):].lstrip('.'),r.strip())) return out def collect(t): ip,nm,iface=t;args=creds(iface) d={'ip':ip,'name':nm,'ok':False} bb=walk(args,ip,'1.3.6.1.2.1.17.1.1.0') if not bb: return d hb=hexmac(bb[0].split(' = ',1)[1]) selfmac=mac(hb[-6:]) if len(hb)>=6 else '' d['selfmac']=selfmac bp={} for suf,val in parse(walk(args,ip,'1.3.6.1.2.1.17.1.4.1.2'),'1.3.6.1.2.1.17.1.4.1.2'): bp[num(suf.split('.')[-1])]=num(val) ifn={} for suf,val in parse(walk(args,ip,'1.3.6.1.2.1.31.1.1.1.1'),'1.3.6.1.2.1.31.1.1.1.1'): ifn[num(suf.split('.')[-1])]=strval(val) st={} for suf,val in parse(walk(args,ip,'1.3.6.1.2.1.17.2.15.1.3'),'1.3.6.1.2.1.17.2.15.1.3'): st[num(suf.split('.')[-1])]=num(val) db={} for suf,val in parse(walk(args,ip,'1.3.6.1.2.1.17.2.15.1.8'),'1.3.6.1.2.1.17.2.15.1.8'): b=hexmac(val) if len(b)>=8: db[num(suf.split('.')[-1])]=(mac(b[2:8]), b[0]*256+b[1]) dp={} for suf,val in parse(walk(args,ip,'1.3.6.1.2.1.17.2.15.1.9'),'1.3.6.1.2.1.17.2.15.1.9'): b=hexmac(val) if len(b)>=2: dp[num(suf.split('.')[-1])]=b[-1] d.update({'ok':True,'bp':bp,'ifn':ifn,'st':st,'db':db,'dp':dp}) return d res=[] with cf.ThreadPoolExecutor(max_workers=30) as ex: for r in ex.map(collect,tasks): res.append(r) ok=[r for r in res if r.get('ok') and r.get('selfmac')] sw_by_mac={r['selfmac']:r for r in ok} print("devices=",len(tasks)," ok_with_stp=",len(ok)," no_bridge=",len(tasks)-len(ok)) edges=[];seen=set() for r in ok: for port,(dbmac,dbpri) in r['db'].items(): s=r['st'].get(port,0) nb=sw_by_mac.get(dbmac) if nb is None or dbmac==r['selfmac']: continue if s not in (4,5): continue dpn=r['dp'].get(port) # our side port name ifx=r['bp'].get(port); myport=r['ifn'].get(ifx,'brport%d'%port) # neighbor side port from designated port number nbifx=nb['bp'].get(dpn); nbport=nb['ifn'].get(nbifx,'brport%s'%dpn) if dpn is not None else '?' key=(r['selfmac'],dbmac,dpn) if key in seen: continue seen.add(key) edges.append([r['name'],r['ip'],myport,r['st'].get(port,''),nb['name'],nb['ip'],nbport,str(dpn)]) # root detection: switches never appear as downstream with a parent? compute which macs never have a parent (their all desig=self) has_parent=set(e[1] for e in edges) # nb mac (parent) appears as to_switch ip # degree deg=collections.Counter() for e in edges: deg[e[1]]+=1;deg[e[5]]+=1 print("stp_edges=",len(edges)) print("degree_dist=",dict(sorted(collections.Counter(deg.values()).items()))) print("\n-- sample 60 edges --") for e in edges[:60]: print(f" {e[0][:38]:38s}[{e[2]:14s} s{e[3]}] --> {e[4][:34]:34s}[{e[6]:14s}] dp={e[7]}") with open('/tmp/stp_topology.tsv','w',encoding='utf-8') as f: f.write('down_switch\tdown_ip\tdown_port\tdown_state\tup_switch\tup_ip\tup_port\tdesig_port\n') for e in edges: f.write('\t'.join(map(str,e))+'\n') # switches with no STP edges withe=set([e[1] for e in edges]+[e[5] for e in edges]) noedge=[r for r in ok if r['ip'] not in withe] print("\nswitches_with_no_STP_edge=",len(noedge)) for r in noedge[:15]: print(" ",r['name'],r['ip'])