stats.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: GPL-2.0
  3. """
  4. Tests related to standard netdevice statistics.
  5. """
  6. import errno
  7. import subprocess
  8. import time
  9. from lib.py import ksft_run, ksft_exit, ksft_pr
  10. from lib.py import ksft_ge, ksft_eq, ksft_is, ksft_in, ksft_lt, ksft_true, ksft_raises
  11. from lib.py import KsftSkipEx, KsftFailEx
  12. from lib.py import ksft_disruptive
  13. from lib.py import EthtoolFamily, NetdevFamily, RtnlFamily, NlError
  14. from lib.py import NetDrvEnv
  15. from lib.py import cmd, ip, defer
  16. ethnl = EthtoolFamily()
  17. netfam = NetdevFamily()
  18. rtnl = RtnlFamily()
  19. def check_pause(cfg) -> None:
  20. """
  21. Check that drivers which support Pause config also report standard
  22. pause stats.
  23. """
  24. try:
  25. ethnl.pause_get({"header": {"dev-index": cfg.ifindex}})
  26. except NlError as e:
  27. if e.error == errno.EOPNOTSUPP:
  28. raise KsftSkipEx("pause not supported by the device") from e
  29. raise
  30. data = ethnl.pause_get({"header": {"dev-index": cfg.ifindex,
  31. "flags": {'stats'}}})
  32. ksft_true(data['stats'], "driver does not report stats")
  33. def check_fec(cfg) -> None:
  34. """
  35. Check that drivers which support FEC config also report standard
  36. FEC stats.
  37. """
  38. try:
  39. ethnl.fec_get({"header": {"dev-index": cfg.ifindex}})
  40. except NlError as e:
  41. if e.error == errno.EOPNOTSUPP:
  42. raise KsftSkipEx("FEC not supported by the device") from e
  43. raise
  44. data = ethnl.fec_get({"header": {"dev-index": cfg.ifindex,
  45. "flags": {'stats'}}})
  46. ksft_true(data['stats'], "driver does not report stats")
  47. def check_fec_hist(cfg) -> None:
  48. """
  49. Check that drivers which support FEC histogram statistics report
  50. reasonable values.
  51. """
  52. try:
  53. data = ethnl.fec_get({"header": {"dev-index": cfg.ifindex,
  54. "flags": {'stats'}}})
  55. except NlError as e:
  56. if e.error == errno.EOPNOTSUPP:
  57. raise KsftSkipEx("FEC not supported by the device") from e
  58. raise
  59. if 'stats' not in data:
  60. raise KsftSkipEx("FEC stats not supported by the device")
  61. if 'hist' not in data['stats']:
  62. raise KsftSkipEx("FEC histogram not supported by the device")
  63. hist = data['stats']['hist']
  64. for fec_bin in hist:
  65. for key in ['bin-low', 'bin-high', 'bin-val']:
  66. ksft_in(key, fec_bin,
  67. "Drivers should always report FEC bin range and value")
  68. ksft_ge(fec_bin['bin-high'], fec_bin['bin-low'],
  69. "FEC bin range should be valid")
  70. if 'bin-val-per-lane' in fec_bin:
  71. ksft_eq(sum(fec_bin['bin-val-per-lane']), fec_bin['bin-val'],
  72. "FEC bin value should be equal to sum of per-plane values")
  73. def pkt_byte_sum(cfg) -> None:
  74. """
  75. Check that qstat and interface stats match in value.
  76. """
  77. def get_qstat(test):
  78. stats = netfam.qstats_get({}, dump=True)
  79. if stats:
  80. for qs in stats:
  81. if qs["ifindex"]== test.ifindex:
  82. return qs
  83. return None
  84. qstat = get_qstat(cfg)
  85. if qstat is None:
  86. raise KsftSkipEx("qstats not supported by the device")
  87. for key in ['tx-packets', 'tx-bytes', 'rx-packets', 'rx-bytes']:
  88. ksft_in(key, qstat, "Drivers should always report basic keys")
  89. # Compare stats, rtnl stats and qstats must match,
  90. # but the interface may be up, so do a series of dumps
  91. # each time the more "recent" stats must be higher or same.
  92. def stat_cmp(rstat, qstat):
  93. for key in ['tx-packets', 'tx-bytes', 'rx-packets', 'rx-bytes']:
  94. if rstat[key] != qstat[key]:
  95. return rstat[key] - qstat[key]
  96. return 0
  97. for _ in range(10):
  98. rtstat = rtnl.getlink({"ifi-index": cfg.ifindex})['stats64']
  99. if stat_cmp(rtstat, qstat) < 0:
  100. raise KsftFailEx("RTNL stats are lower, fetched later")
  101. qstat = get_qstat(cfg)
  102. if stat_cmp(rtstat, qstat) > 0:
  103. raise KsftFailEx("Qstats are lower, fetched later")
  104. def qstat_by_ifindex(cfg) -> None:
  105. """ Qstats Netlink API tests - querying by ifindex. """
  106. # Construct a map ifindex -> [dump, by-index, dump]
  107. ifindexes = {}
  108. stats = netfam.qstats_get({}, dump=True)
  109. for entry in stats:
  110. ifindexes[entry['ifindex']] = [entry, None, None]
  111. for ifindex in ifindexes:
  112. entry = netfam.qstats_get({"ifindex": ifindex}, dump=True)
  113. ksft_eq(len(entry), 1)
  114. ifindexes[entry[0]['ifindex']][1] = entry[0]
  115. stats = netfam.qstats_get({}, dump=True)
  116. for entry in stats:
  117. ifindexes[entry['ifindex']][2] = entry
  118. if len(ifindexes) == 0:
  119. raise KsftSkipEx("No ifindex supports qstats")
  120. # Now make sure the stats match/make sense
  121. for ifindex, triple in ifindexes.items():
  122. all_keys = triple[0].keys() | triple[1].keys() | triple[2].keys()
  123. for key in all_keys:
  124. ksft_ge(triple[1][key], triple[0][key], comment="bad key: " + key)
  125. ksft_ge(triple[2][key], triple[1][key], comment="bad key: " + key)
  126. # Sanity check the dumps
  127. queues = NetdevFamily(recv_size=4096).qstats_get({"scope": "queue"}, dump=True)
  128. # Reformat the output into {ifindex: {rx: [id, id, ...], tx: [id, id, ...]}}
  129. parsed = {}
  130. for entry in queues:
  131. ifindex = entry["ifindex"]
  132. if ifindex not in parsed:
  133. parsed[ifindex] = {"rx":[], "tx": []}
  134. parsed[ifindex][entry["queue-type"]].append(entry['queue-id'])
  135. # Now, validate
  136. for ifindex, queues in parsed.items():
  137. for qtype in ['rx', 'tx']:
  138. ksft_eq(len(queues[qtype]), len(set(queues[qtype])),
  139. comment="repeated queue keys")
  140. ksft_eq(len(queues[qtype]), max(queues[qtype]) + 1,
  141. comment="missing queue keys")
  142. # Test invalid dumps
  143. # 0 is invalid
  144. with ksft_raises(NlError) as cm:
  145. netfam.qstats_get({"ifindex": 0}, dump=True)
  146. ksft_eq(cm.exception.nl_msg.error, -34)
  147. ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.ifindex')
  148. # loopback has no stats
  149. with ksft_raises(NlError) as cm:
  150. netfam.qstats_get({"ifindex": 1}, dump=True)
  151. ksft_eq(cm.exception.nl_msg.error, -errno.EOPNOTSUPP)
  152. ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.ifindex')
  153. # Try to get stats for lowest unused ifindex but not 0
  154. devs = rtnl.getlink({}, dump=True)
  155. all_ifindexes = set(dev["ifi-index"] for dev in devs)
  156. lowest = 2
  157. while lowest in all_ifindexes:
  158. lowest += 1
  159. with ksft_raises(NlError) as cm:
  160. netfam.qstats_get({"ifindex": lowest}, dump=True)
  161. ksft_eq(cm.exception.nl_msg.error, -19)
  162. ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.ifindex')
  163. @ksft_disruptive
  164. def check_down(cfg) -> None:
  165. """ Test statistics (interface and qstat) are not impacted by ifdown """
  166. try:
  167. qstat = netfam.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
  168. except NlError as e:
  169. if e.error == errno.EOPNOTSUPP:
  170. raise KsftSkipEx("qstats not supported by the device") from e
  171. raise
  172. ip(f"link set dev {cfg.dev['ifname']} down")
  173. defer(ip, f"link set dev {cfg.dev['ifname']} up")
  174. qstat2 = netfam.qstats_get({"ifindex": cfg.ifindex}, dump=True)[0]
  175. for k in qstat:
  176. ksft_ge(qstat2[k], qstat[k], comment=f"{k} went backwards on device down")
  177. # exercise per-queue API to make sure that "device down" state
  178. # is handled correctly and doesn't crash
  179. netfam.qstats_get({"ifindex": cfg.ifindex, "scope": "queue"}, dump=True)
  180. def __run_inf_loop(body):
  181. body = body.strip()
  182. if body[-1] != ';':
  183. body += ';'
  184. return subprocess.Popen(f"while true; do {body} done", shell=True,
  185. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  186. def __stats_increase_sanely(old, new) -> None:
  187. for k in old.keys():
  188. ksft_ge(new[k], old[k])
  189. ksft_lt(new[k] - old[k], 1 << 31, comment="likely wrapping error")
  190. def procfs_hammer(cfg) -> None:
  191. """
  192. Reading stats via procfs only holds the RCU lock, which is not an exclusive
  193. lock, make sure drivers can handle parallel reads of stats.
  194. """
  195. one = __run_inf_loop("cat /proc/net/dev")
  196. defer(one.kill)
  197. two = __run_inf_loop("cat /proc/net/dev")
  198. defer(two.kill)
  199. time.sleep(1)
  200. # Make sure the processes are running
  201. ksft_is(one.poll(), None)
  202. ksft_is(two.poll(), None)
  203. rtstat1 = rtnl.getlink({"ifi-index": cfg.ifindex})['stats64']
  204. time.sleep(2)
  205. rtstat2 = rtnl.getlink({"ifi-index": cfg.ifindex})['stats64']
  206. __stats_increase_sanely(rtstat1, rtstat2)
  207. # defers will kill the loops
  208. @ksft_disruptive
  209. def procfs_downup_hammer(cfg) -> None:
  210. """
  211. Reading stats via procfs only holds the RCU lock, drivers often try
  212. to sleep when reading the stats, or don't protect against races.
  213. """
  214. # Set a large number of queues,
  215. # we'll flip between min(max_queues, 64) and 1
  216. channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
  217. if channels['combined-count'] == 0:
  218. rx_type = 'rx'
  219. else:
  220. rx_type = 'combined'
  221. cur_queue_cnt = channels[f'{rx_type}-count']
  222. max_queue_cnt = min(channels[f'{rx_type}-max'], 64)
  223. cmd(f"ethtool -L {cfg.ifname} {rx_type} {max_queue_cnt}")
  224. defer(cmd, f"ethtool -L {cfg.ifname} {rx_type} {cur_queue_cnt}")
  225. # Real test stats
  226. stats = __run_inf_loop("cat /proc/net/dev")
  227. defer(stats.kill)
  228. ipset = f"ip link set dev {cfg.ifname}"
  229. defer(ip, f"link set dev {cfg.ifname} up")
  230. # The "echo -n 1" lets us count iterations below
  231. updown = f"{ipset} down; sleep 0.05; {ipset} up; sleep 0.05; " + \
  232. f"ethtool -L {cfg.ifname} {rx_type} 1; " + \
  233. f"ethtool -L {cfg.ifname} {rx_type} {max_queue_cnt}; " + \
  234. "echo -n 1"
  235. updown = __run_inf_loop(updown)
  236. kill_updown = defer(updown.kill)
  237. time.sleep(1)
  238. # Make sure the processes are running
  239. ksft_is(stats.poll(), None)
  240. ksft_is(updown.poll(), None)
  241. rtstat1 = rtnl.getlink({"ifi-index": cfg.ifindex})['stats64']
  242. # We're looking for crashes, give it extra time
  243. time.sleep(9)
  244. rtstat2 = rtnl.getlink({"ifi-index": cfg.ifindex})['stats64']
  245. __stats_increase_sanely(rtstat1, rtstat2)
  246. kill_updown.exec()
  247. stdout, _ = updown.communicate(timeout=5)
  248. ksft_pr("completed up/down cycles:", len(stdout.decode('utf-8')))
  249. def main() -> None:
  250. """ Ksft boiler plate main """
  251. with NetDrvEnv(__file__, queue_count=100) as cfg:
  252. ksft_run([check_pause, check_fec, check_fec_hist, pkt_byte_sum,
  253. qstat_by_ifindex, check_down, procfs_hammer,
  254. procfs_downup_hammer],
  255. args=(cfg, ))
  256. ksft_exit()
  257. if __name__ == "__main__":
  258. main()