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