| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- 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'])
|