stp_collect.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import json,urllib.request,subprocess,re,collections
  2. import concurrent.futures as cf
  3. import l2cfg
  4. TOK='d3abb125eb301a021ef80a2847a803a7cd2d0b1d3c8337a14d09db9d571369b5'
  5. API='http://192.168.87.20:8080/api_jsonrpc.php'
  6. def call(m,p):
  7. d=json.dumps({"jsonrpc":"2.0","method":m,"params":p,"id":1}).encode()
  8. q=urllib.request.Request(API,data=d,headers={'Content-Type':'application/json','Authorization':'Bearer '+TOK})
  9. return json.loads(urllib.request.urlopen(q,timeout=60).read().decode())
  10. 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'}
  11. hs=l2cfg.hosts(call, output=['hostid','name'])
  12. def creds(i):
  13. det=i.get('details') or {};ver=str(det.get('version','1'))
  14. if ver=='3':
  15. lvl=int(det.get('securitylevel',0));a=['-v3','-l',{0:'noAuthNoPriv',1:'authNoPriv',2:'authPriv'}.get(lvl,'authPriv'),'-u',det.get('securityname','')]
  16. if lvl>=1:a+=['-a',AUTH.get(int(det.get('authprotocol',0)),'SHA'),'-A',det.get('authpassphrase','')]
  17. if lvl>=2:a+=['-x',PRIV.get(int(det.get('privprotocol',0)),'AES'),'-X',det.get('privpassphrase','')]
  18. return a
  19. return ['-v2c','-c',det.get('community','public')]
  20. tasks=[]
  21. for h in hs:
  22. for i in h['interfaces']:
  23. if i.get('type')=='2': tasks.append((i['ip'],h['name'],i));break
  24. def walk(args,ip,oid):
  25. try:
  26. r=subprocess.run(['snmpbulkwalk','-On']+args+['-t','3','-r','1',ip,oid],capture_output=True,text=True,timeout=40)
  27. return [l for l in r.stdout.splitlines() if ' = ' in l and 'No Such' not in l]
  28. except: return []
  29. def num(v):
  30. m=re.search(r'-?\d+',v);return int(m.group()) if m else None
  31. def hexmac(v):
  32. # value like Hex-STRING: 80 00 6C 71 D2 10 F6 00
  33. if ':' in v: v=v.split(':',1)[1]
  34. parts=re.findall(r'[0-9A-Fa-f]{2}',v)
  35. return [int(p,16) for p in parts]
  36. def mac(p): return ':'.join('%02x'%b for b in p)
  37. def strval(v):
  38. return v.split(':',1)[1].strip().strip('"') if ':' in v else v.strip()
  39. def parse(lines,base):
  40. out=[]
  41. for ln in lines:
  42. l,r=ln.split(' = ',1);o=l.strip().lstrip('.')
  43. if o.startswith(base): out.append((o[len(base):].lstrip('.'),r.strip()))
  44. return out
  45. def collect(t):
  46. ip,nm,iface=t;args=creds(iface)
  47. d={'ip':ip,'name':nm,'ok':False}
  48. bb=walk(args,ip,'1.3.6.1.2.1.17.1.1.0')
  49. if not bb: return d
  50. hb=hexmac(bb[0].split(' = ',1)[1])
  51. selfmac=mac(hb[-6:]) if len(hb)>=6 else ''
  52. d['selfmac']=selfmac
  53. bp={}
  54. 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'):
  55. bp[num(suf.split('.')[-1])]=num(val)
  56. ifn={}
  57. 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'):
  58. ifn[num(suf.split('.')[-1])]=strval(val)
  59. st={}
  60. 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'):
  61. st[num(suf.split('.')[-1])]=num(val)
  62. db={}
  63. 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'):
  64. b=hexmac(val)
  65. if len(b)>=8: db[num(suf.split('.')[-1])]=(mac(b[2:8]), b[0]*256+b[1])
  66. dp={}
  67. 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'):
  68. b=hexmac(val)
  69. if len(b)>=2: dp[num(suf.split('.')[-1])]=b[-1]
  70. d.update({'ok':True,'bp':bp,'ifn':ifn,'st':st,'db':db,'dp':dp})
  71. return d
  72. res=[]
  73. with cf.ThreadPoolExecutor(max_workers=30) as ex:
  74. for r in ex.map(collect,tasks): res.append(r)
  75. ok=[r for r in res if r.get('ok') and r.get('selfmac')]
  76. sw_by_mac={r['selfmac']:r for r in ok}
  77. print("devices=",len(tasks)," ok_with_stp=",len(ok)," no_bridge=",len(tasks)-len(ok))
  78. edges=[];seen=set()
  79. for r in ok:
  80. for port,(dbmac,dbpri) in r['db'].items():
  81. s=r['st'].get(port,0)
  82. nb=sw_by_mac.get(dbmac)
  83. if nb is None or dbmac==r['selfmac']: continue
  84. if s not in (4,5): continue
  85. dpn=r['dp'].get(port)
  86. # our side port name
  87. ifx=r['bp'].get(port); myport=r['ifn'].get(ifx,'brport%d'%port)
  88. # neighbor side port from designated port number
  89. nbifx=nb['bp'].get(dpn); nbport=nb['ifn'].get(nbifx,'brport%s'%dpn) if dpn is not None else '?'
  90. key=(r['selfmac'],dbmac,dpn)
  91. if key in seen: continue
  92. seen.add(key)
  93. edges.append([r['name'],r['ip'],myport,r['st'].get(port,''),nb['name'],nb['ip'],nbport,str(dpn)])
  94. # root detection: switches never appear as downstream with a parent? compute which macs never have a parent (their all desig=self)
  95. has_parent=set(e[1] for e in edges) # nb mac (parent) appears as to_switch ip
  96. # degree
  97. deg=collections.Counter()
  98. for e in edges: deg[e[1]]+=1;deg[e[5]]+=1
  99. print("stp_edges=",len(edges))
  100. print("degree_dist=",dict(sorted(collections.Counter(deg.values()).items())))
  101. print("\n-- sample 60 edges --")
  102. for e in edges[:60]:
  103. print(f" {e[0][:38]:38s}[{e[2]:14s} s{e[3]}] --> {e[4][:34]:34s}[{e[6]:14s}] dp={e[7]}")
  104. with open('/tmp/stp_topology.tsv','w',encoding='utf-8') as f:
  105. f.write('down_switch\tdown_ip\tdown_port\tdown_state\tup_switch\tup_ip\tup_port\tdesig_port\n')
  106. for e in edges: f.write('\t'.join(map(str,e))+'\n')
  107. # switches with no STP edges
  108. withe=set([e[1] for e in edges]+[e[5] for e in edges])
  109. noedge=[r for r in ok if r['ip'] not in withe]
  110. print("\nswitches_with_no_STP_edge=",len(noedge))
  111. for r in noedge[:15]: print(" ",r['name'],r['ip'])