1 | // SPDX-License-Identifier: GPL-2.0 |
2 | |
3 | #include <linux/debugfs.h> |
4 | |
5 | #include "netdevsim.h" |
6 | |
7 | #define NSIM_DEV_HWSTATS_TRAFFIC_MS 100 |
8 | |
9 | static struct list_head * |
10 | nsim_dev_hwstats_get_list_head(struct nsim_dev_hwstats *hwstats, |
11 | enum netdev_offload_xstats_type type) |
12 | { |
13 | switch (type) { |
14 | case NETDEV_OFFLOAD_XSTATS_TYPE_L3: |
15 | return &hwstats->l3_list; |
16 | } |
17 | |
18 | WARN_ON_ONCE(1); |
19 | return NULL; |
20 | } |
21 | |
22 | static void nsim_dev_hwstats_traffic_bump(struct nsim_dev_hwstats *hwstats, |
23 | enum netdev_offload_xstats_type type) |
24 | { |
25 | struct nsim_dev_hwstats_netdev *hwsdev; |
26 | struct list_head *hwsdev_list; |
27 | |
28 | hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type); |
29 | if (WARN_ON(!hwsdev_list)) |
30 | return; |
31 | |
32 | list_for_each_entry(hwsdev, hwsdev_list, list) { |
33 | if (hwsdev->enabled) { |
34 | hwsdev->stats.rx_packets += 1; |
35 | hwsdev->stats.tx_packets += 2; |
36 | hwsdev->stats.rx_bytes += 100; |
37 | hwsdev->stats.tx_bytes += 300; |
38 | } |
39 | } |
40 | } |
41 | |
42 | static void nsim_dev_hwstats_traffic_work(struct work_struct *work) |
43 | { |
44 | struct nsim_dev_hwstats *hwstats; |
45 | |
46 | hwstats = container_of(work, struct nsim_dev_hwstats, traffic_dw.work); |
47 | mutex_lock(&hwstats->hwsdev_list_lock); |
48 | nsim_dev_hwstats_traffic_bump(hwstats, type: NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
49 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
50 | |
51 | schedule_delayed_work(dwork: &hwstats->traffic_dw, |
52 | delay: msecs_to_jiffies(NSIM_DEV_HWSTATS_TRAFFIC_MS)); |
53 | } |
54 | |
55 | static struct nsim_dev_hwstats_netdev * |
56 | nsim_dev_hwslist_find_hwsdev(struct list_head *hwsdev_list, |
57 | int ifindex) |
58 | { |
59 | struct nsim_dev_hwstats_netdev *hwsdev; |
60 | |
61 | list_for_each_entry(hwsdev, hwsdev_list, list) { |
62 | if (hwsdev->netdev->ifindex == ifindex) |
63 | return hwsdev; |
64 | } |
65 | |
66 | return NULL; |
67 | } |
68 | |
69 | static int nsim_dev_hwsdev_enable(struct nsim_dev_hwstats_netdev *hwsdev, |
70 | struct netlink_ext_ack *extack) |
71 | { |
72 | if (hwsdev->fail_enable) { |
73 | hwsdev->fail_enable = false; |
74 | NL_SET_ERR_MSG_MOD(extack, "Stats enablement set to fail" ); |
75 | return -ECANCELED; |
76 | } |
77 | |
78 | hwsdev->enabled = true; |
79 | return 0; |
80 | } |
81 | |
82 | static void nsim_dev_hwsdev_disable(struct nsim_dev_hwstats_netdev *hwsdev) |
83 | { |
84 | hwsdev->enabled = false; |
85 | memset(&hwsdev->stats, 0, sizeof(hwsdev->stats)); |
86 | } |
87 | |
88 | static int |
89 | nsim_dev_hwsdev_report_delta(struct nsim_dev_hwstats_netdev *hwsdev, |
90 | struct netdev_notifier_offload_xstats_info *info) |
91 | { |
92 | netdev_offload_xstats_report_delta(rd: info->report_delta, stats: &hwsdev->stats); |
93 | memset(&hwsdev->stats, 0, sizeof(hwsdev->stats)); |
94 | return 0; |
95 | } |
96 | |
97 | static void |
98 | nsim_dev_hwsdev_report_used(struct nsim_dev_hwstats_netdev *hwsdev, |
99 | struct netdev_notifier_offload_xstats_info *info) |
100 | { |
101 | if (hwsdev->enabled) |
102 | netdev_offload_xstats_report_used(ru: info->report_used); |
103 | } |
104 | |
105 | static int nsim_dev_hwstats_event_off_xstats(struct nsim_dev_hwstats *hwstats, |
106 | struct net_device *dev, |
107 | unsigned long event, void *ptr) |
108 | { |
109 | struct netdev_notifier_offload_xstats_info *info; |
110 | struct nsim_dev_hwstats_netdev *hwsdev; |
111 | struct list_head *hwsdev_list; |
112 | int err = 0; |
113 | |
114 | info = ptr; |
115 | hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type: info->type); |
116 | if (!hwsdev_list) |
117 | return 0; |
118 | |
119 | mutex_lock(&hwstats->hwsdev_list_lock); |
120 | |
121 | hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex: dev->ifindex); |
122 | if (!hwsdev) |
123 | goto out; |
124 | |
125 | switch (event) { |
126 | case NETDEV_OFFLOAD_XSTATS_ENABLE: |
127 | err = nsim_dev_hwsdev_enable(hwsdev, extack: info->info.extack); |
128 | break; |
129 | case NETDEV_OFFLOAD_XSTATS_DISABLE: |
130 | nsim_dev_hwsdev_disable(hwsdev); |
131 | break; |
132 | case NETDEV_OFFLOAD_XSTATS_REPORT_USED: |
133 | nsim_dev_hwsdev_report_used(hwsdev, info); |
134 | break; |
135 | case NETDEV_OFFLOAD_XSTATS_REPORT_DELTA: |
136 | err = nsim_dev_hwsdev_report_delta(hwsdev, info); |
137 | break; |
138 | } |
139 | |
140 | out: |
141 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
142 | return err; |
143 | } |
144 | |
145 | static void nsim_dev_hwsdev_fini(struct nsim_dev_hwstats_netdev *hwsdev) |
146 | { |
147 | dev_put(dev: hwsdev->netdev); |
148 | kfree(objp: hwsdev); |
149 | } |
150 | |
151 | static void |
152 | __nsim_dev_hwstats_event_unregister(struct nsim_dev_hwstats *hwstats, |
153 | struct net_device *dev, |
154 | enum netdev_offload_xstats_type type) |
155 | { |
156 | struct nsim_dev_hwstats_netdev *hwsdev; |
157 | struct list_head *hwsdev_list; |
158 | |
159 | hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type); |
160 | if (WARN_ON(!hwsdev_list)) |
161 | return; |
162 | |
163 | hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex: dev->ifindex); |
164 | if (!hwsdev) |
165 | return; |
166 | |
167 | list_del(entry: &hwsdev->list); |
168 | nsim_dev_hwsdev_fini(hwsdev); |
169 | } |
170 | |
171 | static void nsim_dev_hwstats_event_unregister(struct nsim_dev_hwstats *hwstats, |
172 | struct net_device *dev) |
173 | { |
174 | mutex_lock(&hwstats->hwsdev_list_lock); |
175 | __nsim_dev_hwstats_event_unregister(hwstats, dev, |
176 | type: NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
177 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
178 | } |
179 | |
180 | static int nsim_dev_hwstats_event(struct nsim_dev_hwstats *hwstats, |
181 | struct net_device *dev, |
182 | unsigned long event, void *ptr) |
183 | { |
184 | switch (event) { |
185 | case NETDEV_OFFLOAD_XSTATS_ENABLE: |
186 | case NETDEV_OFFLOAD_XSTATS_DISABLE: |
187 | case NETDEV_OFFLOAD_XSTATS_REPORT_USED: |
188 | case NETDEV_OFFLOAD_XSTATS_REPORT_DELTA: |
189 | return nsim_dev_hwstats_event_off_xstats(hwstats, dev, |
190 | event, ptr); |
191 | case NETDEV_UNREGISTER: |
192 | nsim_dev_hwstats_event_unregister(hwstats, dev); |
193 | break; |
194 | } |
195 | |
196 | return 0; |
197 | } |
198 | |
199 | static int nsim_dev_netdevice_event(struct notifier_block *nb, |
200 | unsigned long event, void *ptr) |
201 | { |
202 | struct net_device *dev = netdev_notifier_info_to_dev(info: ptr); |
203 | struct nsim_dev_hwstats *hwstats; |
204 | int err = 0; |
205 | |
206 | hwstats = container_of(nb, struct nsim_dev_hwstats, netdevice_nb); |
207 | err = nsim_dev_hwstats_event(hwstats, dev, event, ptr); |
208 | if (err) |
209 | return notifier_from_errno(err); |
210 | |
211 | return NOTIFY_OK; |
212 | } |
213 | |
214 | static int |
215 | nsim_dev_hwstats_enable_ifindex(struct nsim_dev_hwstats *hwstats, |
216 | int ifindex, |
217 | enum netdev_offload_xstats_type type, |
218 | struct list_head *hwsdev_list) |
219 | { |
220 | struct nsim_dev_hwstats_netdev *hwsdev; |
221 | struct nsim_dev *nsim_dev; |
222 | struct net_device *netdev; |
223 | bool notify = false; |
224 | struct net *net; |
225 | int err = 0; |
226 | |
227 | nsim_dev = container_of(hwstats, struct nsim_dev, hwstats); |
228 | net = nsim_dev_net(nsim_dev); |
229 | |
230 | rtnl_lock(); |
231 | mutex_lock(&hwstats->hwsdev_list_lock); |
232 | hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex); |
233 | if (hwsdev) |
234 | goto out_unlock_list; |
235 | |
236 | netdev = dev_get_by_index(net, ifindex); |
237 | if (!netdev) { |
238 | err = -ENODEV; |
239 | goto out_unlock_list; |
240 | } |
241 | |
242 | hwsdev = kzalloc(size: sizeof(*hwsdev), GFP_KERNEL); |
243 | if (!hwsdev) { |
244 | err = -ENOMEM; |
245 | goto out_put_netdev; |
246 | } |
247 | |
248 | hwsdev->netdev = netdev; |
249 | list_add_tail(new: &hwsdev->list, head: hwsdev_list); |
250 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
251 | |
252 | if (netdev_offload_xstats_enabled(dev: netdev, type)) { |
253 | nsim_dev_hwsdev_enable(hwsdev, NULL); |
254 | notify = true; |
255 | } |
256 | |
257 | if (notify) |
258 | rtnl_offload_xstats_notify(dev: netdev); |
259 | rtnl_unlock(); |
260 | return err; |
261 | |
262 | out_put_netdev: |
263 | dev_put(dev: netdev); |
264 | out_unlock_list: |
265 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
266 | rtnl_unlock(); |
267 | return err; |
268 | } |
269 | |
270 | static int |
271 | nsim_dev_hwstats_disable_ifindex(struct nsim_dev_hwstats *hwstats, |
272 | int ifindex, |
273 | enum netdev_offload_xstats_type type, |
274 | struct list_head *hwsdev_list) |
275 | { |
276 | struct nsim_dev_hwstats_netdev *hwsdev; |
277 | int err = 0; |
278 | |
279 | rtnl_lock(); |
280 | mutex_lock(&hwstats->hwsdev_list_lock); |
281 | hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex); |
282 | if (hwsdev) |
283 | list_del(entry: &hwsdev->list); |
284 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
285 | |
286 | if (!hwsdev) { |
287 | err = -ENOENT; |
288 | goto unlock_out; |
289 | } |
290 | |
291 | if (netdev_offload_xstats_enabled(dev: hwsdev->netdev, type)) { |
292 | netdev_offload_xstats_push_delta(dev: hwsdev->netdev, type, |
293 | stats: &hwsdev->stats); |
294 | rtnl_offload_xstats_notify(dev: hwsdev->netdev); |
295 | } |
296 | nsim_dev_hwsdev_fini(hwsdev); |
297 | |
298 | unlock_out: |
299 | rtnl_unlock(); |
300 | return err; |
301 | } |
302 | |
303 | static int |
304 | nsim_dev_hwstats_fail_ifindex(struct nsim_dev_hwstats *hwstats, |
305 | int ifindex, |
306 | enum netdev_offload_xstats_type type, |
307 | struct list_head *hwsdev_list) |
308 | { |
309 | struct nsim_dev_hwstats_netdev *hwsdev; |
310 | int err = 0; |
311 | |
312 | mutex_lock(&hwstats->hwsdev_list_lock); |
313 | |
314 | hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex); |
315 | if (!hwsdev) { |
316 | err = -ENOENT; |
317 | goto err_hwsdev_list_unlock; |
318 | } |
319 | |
320 | hwsdev->fail_enable = true; |
321 | |
322 | err_hwsdev_list_unlock: |
323 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
324 | return err; |
325 | } |
326 | |
327 | enum nsim_dev_hwstats_do { |
328 | NSIM_DEV_HWSTATS_DO_DISABLE, |
329 | NSIM_DEV_HWSTATS_DO_ENABLE, |
330 | NSIM_DEV_HWSTATS_DO_FAIL, |
331 | }; |
332 | |
333 | struct nsim_dev_hwstats_fops { |
334 | const struct file_operations fops; |
335 | enum nsim_dev_hwstats_do action; |
336 | enum netdev_offload_xstats_type type; |
337 | }; |
338 | |
339 | static ssize_t |
340 | nsim_dev_hwstats_do_write(struct file *file, |
341 | const char __user *data, |
342 | size_t count, loff_t *ppos) |
343 | { |
344 | struct nsim_dev_hwstats *hwstats = file->private_data; |
345 | struct nsim_dev_hwstats_fops *hwsfops; |
346 | struct list_head *hwsdev_list; |
347 | int ifindex; |
348 | int err; |
349 | |
350 | hwsfops = container_of(debugfs_real_fops(file), |
351 | struct nsim_dev_hwstats_fops, fops); |
352 | |
353 | err = kstrtoint_from_user(s: data, count, base: 0, res: &ifindex); |
354 | if (err) |
355 | return err; |
356 | |
357 | hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type: hwsfops->type); |
358 | if (WARN_ON(!hwsdev_list)) |
359 | return -EINVAL; |
360 | |
361 | switch (hwsfops->action) { |
362 | case NSIM_DEV_HWSTATS_DO_DISABLE: |
363 | err = nsim_dev_hwstats_disable_ifindex(hwstats, ifindex, |
364 | type: hwsfops->type, |
365 | hwsdev_list); |
366 | break; |
367 | case NSIM_DEV_HWSTATS_DO_ENABLE: |
368 | err = nsim_dev_hwstats_enable_ifindex(hwstats, ifindex, |
369 | type: hwsfops->type, |
370 | hwsdev_list); |
371 | break; |
372 | case NSIM_DEV_HWSTATS_DO_FAIL: |
373 | err = nsim_dev_hwstats_fail_ifindex(hwstats, ifindex, |
374 | type: hwsfops->type, |
375 | hwsdev_list); |
376 | break; |
377 | } |
378 | if (err) |
379 | return err; |
380 | |
381 | return count; |
382 | } |
383 | |
384 | #define NSIM_DEV_HWSTATS_FOPS(ACTION, TYPE) \ |
385 | { \ |
386 | .fops = { \ |
387 | .open = simple_open, \ |
388 | .write = nsim_dev_hwstats_do_write, \ |
389 | .llseek = generic_file_llseek, \ |
390 | .owner = THIS_MODULE, \ |
391 | }, \ |
392 | .action = ACTION, \ |
393 | .type = TYPE, \ |
394 | } |
395 | |
396 | static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_disable_fops = |
397 | NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_DISABLE, |
398 | NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
399 | |
400 | static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_enable_fops = |
401 | NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_ENABLE, |
402 | NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
403 | |
404 | static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_fail_fops = |
405 | NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_FAIL, |
406 | NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
407 | |
408 | #undef NSIM_DEV_HWSTATS_FOPS |
409 | |
410 | int nsim_dev_hwstats_init(struct nsim_dev *nsim_dev) |
411 | { |
412 | struct nsim_dev_hwstats *hwstats = &nsim_dev->hwstats; |
413 | struct net *net = nsim_dev_net(nsim_dev); |
414 | int err; |
415 | |
416 | mutex_init(&hwstats->hwsdev_list_lock); |
417 | INIT_LIST_HEAD(list: &hwstats->l3_list); |
418 | |
419 | hwstats->netdevice_nb.notifier_call = nsim_dev_netdevice_event; |
420 | err = register_netdevice_notifier_net(net, nb: &hwstats->netdevice_nb); |
421 | if (err) |
422 | goto err_mutex_destroy; |
423 | |
424 | hwstats->ddir = debugfs_create_dir(name: "hwstats" , parent: nsim_dev->ddir); |
425 | if (IS_ERR(ptr: hwstats->ddir)) { |
426 | err = PTR_ERR(ptr: hwstats->ddir); |
427 | goto err_unregister_notifier; |
428 | } |
429 | |
430 | hwstats->l3_ddir = debugfs_create_dir(name: "l3" , parent: hwstats->ddir); |
431 | if (IS_ERR(ptr: hwstats->l3_ddir)) { |
432 | err = PTR_ERR(ptr: hwstats->l3_ddir); |
433 | goto err_remove_hwstats_recursive; |
434 | } |
435 | |
436 | debugfs_create_file(name: "enable_ifindex" , mode: 0200, parent: hwstats->l3_ddir, data: hwstats, |
437 | fops: &nsim_dev_hwstats_l3_enable_fops.fops); |
438 | debugfs_create_file(name: "disable_ifindex" , mode: 0200, parent: hwstats->l3_ddir, data: hwstats, |
439 | fops: &nsim_dev_hwstats_l3_disable_fops.fops); |
440 | debugfs_create_file(name: "fail_next_enable" , mode: 0200, parent: hwstats->l3_ddir, data: hwstats, |
441 | fops: &nsim_dev_hwstats_l3_fail_fops.fops); |
442 | |
443 | INIT_DELAYED_WORK(&hwstats->traffic_dw, |
444 | &nsim_dev_hwstats_traffic_work); |
445 | schedule_delayed_work(dwork: &hwstats->traffic_dw, |
446 | delay: msecs_to_jiffies(NSIM_DEV_HWSTATS_TRAFFIC_MS)); |
447 | return 0; |
448 | |
449 | err_remove_hwstats_recursive: |
450 | debugfs_remove_recursive(dentry: hwstats->ddir); |
451 | err_unregister_notifier: |
452 | unregister_netdevice_notifier_net(net, nb: &hwstats->netdevice_nb); |
453 | err_mutex_destroy: |
454 | mutex_destroy(lock: &hwstats->hwsdev_list_lock); |
455 | return err; |
456 | } |
457 | |
458 | static void nsim_dev_hwsdev_list_wipe(struct nsim_dev_hwstats *hwstats, |
459 | enum netdev_offload_xstats_type type) |
460 | { |
461 | struct nsim_dev_hwstats_netdev *hwsdev, *tmp; |
462 | struct list_head *hwsdev_list; |
463 | |
464 | hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type); |
465 | if (WARN_ON(!hwsdev_list)) |
466 | return; |
467 | |
468 | mutex_lock(&hwstats->hwsdev_list_lock); |
469 | list_for_each_entry_safe(hwsdev, tmp, hwsdev_list, list) { |
470 | list_del(entry: &hwsdev->list); |
471 | nsim_dev_hwsdev_fini(hwsdev); |
472 | } |
473 | mutex_unlock(lock: &hwstats->hwsdev_list_lock); |
474 | } |
475 | |
476 | void nsim_dev_hwstats_exit(struct nsim_dev *nsim_dev) |
477 | { |
478 | struct nsim_dev_hwstats *hwstats = &nsim_dev->hwstats; |
479 | struct net *net = nsim_dev_net(nsim_dev); |
480 | |
481 | cancel_delayed_work_sync(dwork: &hwstats->traffic_dw); |
482 | debugfs_remove_recursive(dentry: hwstats->ddir); |
483 | unregister_netdevice_notifier_net(net, nb: &hwstats->netdevice_nb); |
484 | nsim_dev_hwsdev_list_wipe(hwstats, type: NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
485 | mutex_destroy(lock: &hwstats->hwsdev_list_lock); |
486 | } |
487 | |