port_status.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Read-only port status collector: IF-MIB oper/admin/speed + FDB learned MAC per port.
  4. import json, urllib.request, subprocess, re, sys, collections
  5. import concurrent.futures as cf
  6. import l2cfg
  7. TOK = 'd3abb125eb301a021ef80a2847a803a7cd2d0b1d3c8337a14d09db9d571369b5'
  8. API = 'http://192.168.87.20:8080/api_jsonrpc.php'
  9. def call(m, p):
  10. d = json.dumps({"jsonrpc": "2.0", "method": m, "params": p, "id": 1}).encode()
  11. q = urllib.request.Request(API, data=d, headers={'Content-Type': 'application/json', 'Authorization': 'Bearer ' + TOK})
  12. return json.loads(urllib.request.urlopen(q, timeout=60).read().decode())
  13. AUTH = {0: 'MD5', 1: 'SHA', 2: 'SHA-224', 3: 'SHA-256', 4: 'SHA-384', 5: 'SHA-512'}
  14. PRIV = {0: 'DES', 1: 'AES', 2: 'AES-192', 3: 'AES-256', 4: 'AES', 5: 'AES-256'}
  15. def creds(i):
  16. det = i.get('details') or {}; ver = str(det.get('version', '1'))
  17. if ver == '3':
  18. lvl = int(det.get('securitylevel', 0))
  19. a = ['-v3', '-l', {0: 'noAuthNoPriv', 1: 'authNoPriv', 2: 'authPriv'}.get(lvl, 'authPriv'), '-u', det.get('securityname', '')]
  20. if lvl >= 1: a += ['-a', AUTH.get(int(det.get('authprotocol', 0)), 'SHA'), '-A', det.get('authpassphrase', '')]
  21. if lvl >= 2: a += ['-x', PRIV.get(int(det.get('privprotocol', 0)), 'AES'), '-X', det.get('privpassphrase', '')]
  22. return a
  23. return ['-v2c', '-c', det.get('community', 'public')]
  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 Exception:
  29. return []
  30. def num(v):
  31. m = re.search(r'-?\d+', v); return int(m.group()) if m else None
  32. def strval(v):
  33. return v.split(':', 1)[1].strip().strip('"') if ':' in v else v.strip()
  34. O = {
  35. 'ifIndex':'1.3.6.1.2.1.2.2.1.1','ifDescr':'1.3.6.1.2.1.2.2.1.2','ifType':'1.3.6.1.2.1.2.2.1.3',
  36. 'ifSpeed':'1.3.6.1.2.1.2.2.1.5','ifAdmin':'1.3.6.1.2.1.2.2.1.7','ifOper':'1.3.6.1.2.1.2.2.1.8',
  37. 'ifLastChange':'1.3.6.1.2.1.2.2.1.9','ifName':'1.3.6.1.2.1.31.1.1.1.1','ifHighSpeed':'1.3.6.1.2.1.31.1.1.1.15',
  38. 'ifAlias':'1.3.6.1.2.1.31.1.1.1.18','ifConnector':'1.3.6.1.2.1.31.1.1.1.17','duplex':'1.3.6.1.2.1.10.7.2.1.19',
  39. 'fdbPort':'1.3.6.1.2.1.17.4.3.1.2','bpIf':'1.3.6.1.2.1.17.1.4.1.2','fdbStatus':'1.3.6.1.2.1.17.4.3.1.3',
  40. }
  41. def parse(lines, base):
  42. out = []
  43. for ln in lines:
  44. l, r = ln.split(' = ', 1); o = l.strip().lstrip('.')
  45. if o.startswith(base): out.append((o[len(base):].lstrip('.'), r.strip()))
  46. return out
  47. def collect(t):
  48. ip, nm, iface = t; args = creds(iface)
  49. d = {'ip': ip, 'name': nm, 'ok': False}
  50. idx = [n for s, v in parse(walk(args, ip, O['ifIndex']), O['ifIndex']) for n in [num(s.split('.')[-1])] if n is not None]
  51. if not idx:
  52. # fall back to ifName walk presence
  53. fn = parse(walk(args, ip, O['ifName']), O['ifName'])
  54. if not fn: return d
  55. def m1(key):
  56. return {num(s.split('.')[-1]): strval(v) for s, v in parse(walk(args, ip, O[key]), O[key])}
  57. def n1(key):
  58. return {num(s.split('.')[-1]): num(strval(v)) for s, v in parse(walk(args, ip, O[key]), O[key])}
  59. ifname = m1('ifName'); ifdescr = m1('ifDescr'); ifalias = m1('ifAlias')
  60. iftype = n1('ifType'); ifadmin = n1('ifAdmin'); ifoper = n1('ifOper')
  61. ifspeed = n1('ifSpeed'); ifhigh = n1('ifHighSpeed'); lastchg = n1('ifLastChange')
  62. conn = n1('ifConnector'); dup = n1('duplex')
  63. # FDB per port -> count and sample macs
  64. bpif = {num(s.split('.')[-1]): num(v) for s, v in parse(walk(args, ip, O['bpIf']), O['bpIf'])}
  65. st = {num(s.split('.')[-1]): num(v) for s, v in parse(walk(args, ip, O['fdbStatus']), O['fdbStatus'])}
  66. mac_cnt = collections.Counter(); mac_by_if = collections.defaultdict(list)
  67. for s, v in parse(walk(args, ip, O['fdbPort']), O['fdbPort']):
  68. port = num(v)
  69. if port is None: continue
  70. ifx = bpif.get(port)
  71. if ifx is None: continue
  72. mac_cnt[ifx] = mac_cnt.get(ifx, 0) + 1
  73. if len(mac_by_if[ifx]) < 6:
  74. parts = s.split('.')
  75. macs = ':'.join('%02x' % int(p) for p in parts[-6:]) if len(parts) >= 6 else ''
  76. mac_by_if[ifx].append(macs)
  77. d.update({'ok': True, 'ifname': ifname, 'ifdescr': ifdescr, 'ifalias': ifalias, 'iftype': iftype,
  78. 'ifadmin': ifadmin, 'ifoper': ifoper, 'ifspeed': ifspeed, 'ifhigh': ifhigh,
  79. 'lastchg': lastchg, 'conn': conn, 'dup': dup, 'mac_cnt': dict(mac_cnt),
  80. 'mac_by_if': {k: v for k, v in mac_by_if.items()}})
  81. return d
  82. def main():
  83. hs = l2cfg.hosts(call, output=['hostid', 'name'])
  84. tasks = []
  85. for h in hs:
  86. for i in h['interfaces']:
  87. if i.get('type') == '2': tasks.append((i['ip'], h['name'], i)); break
  88. res = []
  89. with cf.ThreadPoolExecutor(max_workers=30) as ex:
  90. for r in ex.map(collect, tasks): res.append(r)
  91. ok = [r for r in res if r.get('ok')]
  92. rows = []
  93. for r in ok:
  94. allif = set(r['ifname']) | set(r['ifdescr'])
  95. for ifx in allif:
  96. it = r['iftype'].get(ifx, 0)
  97. admin = r['ifadmin'].get(ifx); oper = r['ifoper'].get(ifx)
  98. sp = r['ifhigh'].get(ifx)
  99. if sp is None or sp == 0:
  100. s_bps = r['ifspeed'].get(ifx) or 0
  101. sp = int(s_bps / 1000000) if s_bps else 0
  102. rows.append([r['name'], r['ip'], str(ifx), r['ifname'].get(ifx, '') or r['ifdescr'].get(ifx, ''),
  103. r['ifalias'].get(ifx, ''), str(it),
  104. {1:'up',2:'down',3:'testing',4:'unknown',5:'dormant',6:'notPresent',7:'lowerLayerDown'}.get(admin, str(admin)),
  105. {1:'up',2:'down',3:'testing',4:'unknown',5:'dormant',6:'notPresent',7:'lowerLayerDown'}.get(oper, str(oper)),
  106. str(sp if sp is not None else ''),
  107. {1:'half',2:'full',3:'unknown'}.get(r['dup'].get(ifx), ''),
  108. str(r['conn'].get(ifx, '')), str(r['lastchg'].get(ifx, '')),
  109. str(r['mac_cnt'].get(ifx, 0)), ' '.join(r['mac_by_if'].get(ifx, [])[:3])])
  110. with open('/tmp/port_status.tsv', 'w', encoding='utf-8') as f:
  111. f.write('switch_name\tswitch_ip\tifIndex\tifName\tifAlias\tifType\tadmin_status\toper_status\tspeed_mbps\tduplex\tconnector_present\tlast_change_ticks\tmac_count\tsample_macs\n')
  112. for row in rows:
  113. f.write('\t'.join(row) + '\n')
  114. # summary
  115. phys = [r for r in rows if r[5] in ('6', '62', '117', '161', '117')]
  116. up = [r for r in rows if r[7] == 'up']
  117. spd = collections.Counter(r[8] for r in up)
  118. with open('/tmp/port_status_summary.txt', 'w', encoding='utf-8') as f:
  119. f.write('switches_ok=%d/%d\n' % (len(ok), len(tasks)))
  120. f.write('total_if=%d\n' % len(rows))
  121. f.write('oper_up=%d oper_down=%d\n' % (len(up), sum(1 for r in rows if r[7] == 'down')))
  122. f.write('up_speed_dist=%s\n' % dict(spd))
  123. f.write('up_with_mac=%d\n' % sum(1 for r in rows if r[7]=='up' and int(r[12] or 0) > 0))
  124. print(open('/tmp/port_status_summary.txt').read())
  125. if __name__ == '__main__':
  126. main()