port_status.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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. POE_STD = '1.3.6.1.2.1.105.1.1.1.1.1' # RFC 3621 pethPsePortAdminEnable
  42. POE_HW_JUDGE = '1.3.6.1.4.1.2011.5.25.195.4.1.2' # HUAWEI-POE-MIB hwPoePortJudgeIsSupport (TruthValue)
  43. def parse(lines, base):
  44. out = []
  45. for ln in lines:
  46. l, r = ln.split(' = ', 1); o = l.strip().lstrip('.')
  47. if o.startswith(base): out.append((o[len(base):].lstrip('.'), r.strip()))
  48. return out
  49. def collect(t):
  50. ip, nm, iface = t; args = creds(iface)
  51. d = {'ip': ip, 'name': nm, 'ok': False}
  52. 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]
  53. if not idx:
  54. # fall back to ifName walk presence
  55. fn = parse(walk(args, ip, O['ifName']), O['ifName'])
  56. if not fn: return d
  57. def m1(key):
  58. return {num(s.split('.')[-1]): strval(v) for s, v in parse(walk(args, ip, O[key]), O[key])}
  59. def n1(key):
  60. return {num(s.split('.')[-1]): num(strval(v)) for s, v in parse(walk(args, ip, O[key]), O[key])}
  61. ifname = m1('ifName'); ifdescr = m1('ifDescr'); ifalias = m1('ifAlias')
  62. iftype = n1('ifType'); ifadmin = n1('ifAdmin'); ifoper = n1('ifOper')
  63. ifspeed = n1('ifSpeed'); ifhigh = n1('ifHighSpeed'); lastchg = n1('ifLastChange')
  64. conn = n1('ifConnector'); dup = n1('duplex')
  65. # PoE capability: standard RFC 3621 PSE table, plus Huawei's TruthValue
  66. # capability table. A row in the standard table means a PSE port exists;
  67. # Huawei reports true(1)/false(2) for each interface. This is capability,
  68. # not current PD power, so it stays stable when no device is attached.
  69. std_poe = parse(walk(args, ip, POE_STD), POE_STD)
  70. hw_poe = parse(walk(args, ip, POE_HW_JUDGE), POE_HW_JUDGE)
  71. poe = bool(std_poe) or any(num(v) == 1 for _, v in hw_poe)
  72. # FDB per port -> count and sample macs
  73. bpif = {num(s.split('.')[-1]): num(v) for s, v in parse(walk(args, ip, O['bpIf']), O['bpIf'])}
  74. st = {num(s.split('.')[-1]): num(v) for s, v in parse(walk(args, ip, O['fdbStatus']), O['fdbStatus'])}
  75. mac_cnt = collections.Counter(); mac_by_if = collections.defaultdict(list)
  76. for s, v in parse(walk(args, ip, O['fdbPort']), O['fdbPort']):
  77. port = num(v)
  78. if port is None: continue
  79. ifx = bpif.get(port)
  80. if ifx is None: continue
  81. mac_cnt[ifx] = mac_cnt.get(ifx, 0) + 1
  82. if len(mac_by_if[ifx]) < 6:
  83. parts = s.split('.')
  84. macs = ':'.join('%02x' % int(p) for p in parts[-6:]) if len(parts) >= 6 else ''
  85. mac_by_if[ifx].append(macs)
  86. d.update({'ok': True, 'ifname': ifname, 'ifdescr': ifdescr, 'ifalias': ifalias, 'iftype': iftype,
  87. 'ifadmin': ifadmin, 'ifoper': ifoper, 'ifspeed': ifspeed, 'ifhigh': ifhigh,
  88. 'lastchg': lastchg, 'conn': conn, 'dup': dup, 'mac_cnt': dict(mac_cnt),
  89. 'mac_by_if': {k: v for k, v in mac_by_if.items()}, 'poe': poe})
  90. return d
  91. def main():
  92. hs = l2cfg.hosts(call, output=['hostid', 'name'])
  93. tasks = []
  94. for h in hs:
  95. for i in h['interfaces']:
  96. if i.get('type') == '2': tasks.append((i['ip'], h['name'], i)); break
  97. res = []
  98. with cf.ThreadPoolExecutor(max_workers=30) as ex:
  99. for r in ex.map(collect, tasks): res.append(r)
  100. ok = [r for r in res if r.get('ok')]
  101. rows = []
  102. for r in ok:
  103. allif = set(r['ifname']) | set(r['ifdescr'])
  104. for ifx in allif:
  105. it = r['iftype'].get(ifx, 0)
  106. admin = r['ifadmin'].get(ifx); oper = r['ifoper'].get(ifx)
  107. sp = r['ifhigh'].get(ifx)
  108. if sp is None or sp == 0:
  109. s_bps = r['ifspeed'].get(ifx) or 0
  110. sp = int(s_bps / 1000000) if s_bps else 0
  111. rows.append([r['name'], r['ip'], str(ifx), r['ifname'].get(ifx, '') or r['ifdescr'].get(ifx, ''),
  112. r['ifalias'].get(ifx, ''), str(it),
  113. {1:'up',2:'down',3:'testing',4:'unknown',5:'dormant',6:'notPresent',7:'lowerLayerDown'}.get(admin, str(admin)),
  114. {1:'up',2:'down',3:'testing',4:'unknown',5:'dormant',6:'notPresent',7:'lowerLayerDown'}.get(oper, str(oper)),
  115. str(sp if sp is not None else ''),
  116. {1:'half',2:'full',3:'unknown'}.get(r['dup'].get(ifx), ''),
  117. str(r['conn'].get(ifx, '')), str(r['lastchg'].get(ifx, '')),
  118. str(r['mac_cnt'].get(ifx, 0)), ' '.join(r['mac_by_if'].get(ifx, [])[:3])])
  119. with open('/tmp/port_status.tsv', 'w', encoding='utf-8') as f:
  120. 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')
  121. for row in rows:
  122. f.write('\t'.join(row) + '\n')
  123. with open('/tmp/poe_switches.tsv', 'w', encoding='utf-8') as f:
  124. f.write('switch_ip\tpoe\n')
  125. for r in ok:
  126. f.write('%s\t%s\n' % (r['ip'], '1' if r.get('poe') else '0'))
  127. poe_count = sum(1 for r in ok if r.get('poe'))
  128. print('poe_switches=%d' % poe_count)
  129. # summary
  130. phys = [r for r in rows if r[5] in ('6', '62', '117', '161', '117')]
  131. up = [r for r in rows if r[7] == 'up']
  132. spd = collections.Counter(r[8] for r in up)
  133. with open('/tmp/port_status_summary.txt', 'w', encoding='utf-8') as f:
  134. f.write('switches_ok=%d/%d\n' % (len(ok), len(tasks)))
  135. f.write('total_if=%d\n' % len(rows))
  136. f.write('oper_up=%d oper_down=%d\n' % (len(up), sum(1 for r in rows if r[7] == 'down')))
  137. f.write('up_speed_dist=%s\n' % dict(spd))
  138. f.write('up_with_mac=%d\n' % sum(1 for r in rows if r[7]=='up' and int(r[12] or 0) > 0))
  139. print(open('/tmp/port_status_summary.txt').read())
  140. if __name__ == '__main__':
  141. main()