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