| // SPDX-License-Identifier: GPL-2.0 |
| |
| #include <linux/debugfs.h> |
| |
| #include "netdevsim.h" |
| |
| #define NSIM_DEV_HWSTATS_TRAFFIC_MS 100 |
| |
| static struct list_head * |
| nsim_dev_hwstats_get_list_head(struct nsim_dev_hwstats *hwstats, |
| enum netdev_offload_xstats_type type) |
| { |
| switch (type) { |
| case NETDEV_OFFLOAD_XSTATS_TYPE_L3: |
| return &hwstats->l3_list; |
| } |
| |
| WARN_ON_ONCE(1); |
| return NULL; |
| } |
| |
| static void nsim_dev_hwstats_traffic_bump(struct nsim_dev_hwstats *hwstats, |
| enum netdev_offload_xstats_type type) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| struct list_head *hwsdev_list; |
| |
| hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type); |
| if (WARN_ON(!hwsdev_list)) |
| return; |
| |
| list_for_each_entry(hwsdev, hwsdev_list, list) { |
| if (hwsdev->enabled) { |
| hwsdev->stats.rx_packets += 1; |
| hwsdev->stats.tx_packets += 2; |
| hwsdev->stats.rx_bytes += 100; |
| hwsdev->stats.tx_bytes += 300; |
| } |
| } |
| } |
| |
| static void nsim_dev_hwstats_traffic_work(struct work_struct *work) |
| { |
| struct nsim_dev_hwstats *hwstats; |
| |
| hwstats = container_of(work, struct nsim_dev_hwstats, traffic_dw.work); |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| nsim_dev_hwstats_traffic_bump(hwstats, NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| |
| schedule_delayed_work(&hwstats->traffic_dw, |
| msecs_to_jiffies(NSIM_DEV_HWSTATS_TRAFFIC_MS)); |
| } |
| |
| static struct nsim_dev_hwstats_netdev * |
| nsim_dev_hwslist_find_hwsdev(struct list_head *hwsdev_list, |
| int ifindex) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| |
| list_for_each_entry(hwsdev, hwsdev_list, list) { |
| if (hwsdev->netdev->ifindex == ifindex) |
| return hwsdev; |
| } |
| |
| return NULL; |
| } |
| |
| static int nsim_dev_hwsdev_enable(struct nsim_dev_hwstats_netdev *hwsdev, |
| struct netlink_ext_ack *extack) |
| { |
| if (hwsdev->fail_enable) { |
| hwsdev->fail_enable = false; |
| NL_SET_ERR_MSG_MOD(extack, "Stats enablement set to fail"); |
| return -ECANCELED; |
| } |
| |
| hwsdev->enabled = true; |
| return 0; |
| } |
| |
| static void nsim_dev_hwsdev_disable(struct nsim_dev_hwstats_netdev *hwsdev) |
| { |
| hwsdev->enabled = false; |
| memset(&hwsdev->stats, 0, sizeof(hwsdev->stats)); |
| } |
| |
| static int |
| nsim_dev_hwsdev_report_delta(struct nsim_dev_hwstats_netdev *hwsdev, |
| struct netdev_notifier_offload_xstats_info *info) |
| { |
| netdev_offload_xstats_report_delta(info->report_delta, &hwsdev->stats); |
| memset(&hwsdev->stats, 0, sizeof(hwsdev->stats)); |
| return 0; |
| } |
| |
| static void |
| nsim_dev_hwsdev_report_used(struct nsim_dev_hwstats_netdev *hwsdev, |
| struct netdev_notifier_offload_xstats_info *info) |
| { |
| if (hwsdev->enabled) |
| netdev_offload_xstats_report_used(info->report_used); |
| } |
| |
| static int nsim_dev_hwstats_event_off_xstats(struct nsim_dev_hwstats *hwstats, |
| struct net_device *dev, |
| unsigned long event, void *ptr) |
| { |
| struct netdev_notifier_offload_xstats_info *info; |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| struct list_head *hwsdev_list; |
| int err = 0; |
| |
| info = ptr; |
| hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, info->type); |
| if (!hwsdev_list) |
| return 0; |
| |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| |
| hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, dev->ifindex); |
| if (!hwsdev) |
| goto out; |
| |
| switch (event) { |
| case NETDEV_OFFLOAD_XSTATS_ENABLE: |
| err = nsim_dev_hwsdev_enable(hwsdev, info->info.extack); |
| break; |
| case NETDEV_OFFLOAD_XSTATS_DISABLE: |
| nsim_dev_hwsdev_disable(hwsdev); |
| break; |
| case NETDEV_OFFLOAD_XSTATS_REPORT_USED: |
| nsim_dev_hwsdev_report_used(hwsdev, info); |
| break; |
| case NETDEV_OFFLOAD_XSTATS_REPORT_DELTA: |
| err = nsim_dev_hwsdev_report_delta(hwsdev, info); |
| break; |
| } |
| |
| out: |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| return err; |
| } |
| |
| static void nsim_dev_hwsdev_fini(struct nsim_dev_hwstats_netdev *hwsdev) |
| { |
| dev_put(hwsdev->netdev); |
| kfree(hwsdev); |
| } |
| |
| static void |
| __nsim_dev_hwstats_event_unregister(struct nsim_dev_hwstats *hwstats, |
| struct net_device *dev, |
| enum netdev_offload_xstats_type type) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| struct list_head *hwsdev_list; |
| |
| hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type); |
| if (WARN_ON(!hwsdev_list)) |
| return; |
| |
| hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, dev->ifindex); |
| if (!hwsdev) |
| return; |
| |
| list_del(&hwsdev->list); |
| nsim_dev_hwsdev_fini(hwsdev); |
| } |
| |
| static void nsim_dev_hwstats_event_unregister(struct nsim_dev_hwstats *hwstats, |
| struct net_device *dev) |
| { |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| __nsim_dev_hwstats_event_unregister(hwstats, dev, |
| NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| } |
| |
| static int nsim_dev_hwstats_event(struct nsim_dev_hwstats *hwstats, |
| struct net_device *dev, |
| unsigned long event, void *ptr) |
| { |
| switch (event) { |
| case NETDEV_OFFLOAD_XSTATS_ENABLE: |
| case NETDEV_OFFLOAD_XSTATS_DISABLE: |
| case NETDEV_OFFLOAD_XSTATS_REPORT_USED: |
| case NETDEV_OFFLOAD_XSTATS_REPORT_DELTA: |
| return nsim_dev_hwstats_event_off_xstats(hwstats, dev, |
| event, ptr); |
| case NETDEV_UNREGISTER: |
| nsim_dev_hwstats_event_unregister(hwstats, dev); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int nsim_dev_netdevice_event(struct notifier_block *nb, |
| unsigned long event, void *ptr) |
| { |
| struct net_device *dev = netdev_notifier_info_to_dev(ptr); |
| struct nsim_dev_hwstats *hwstats; |
| int err = 0; |
| |
| hwstats = container_of(nb, struct nsim_dev_hwstats, netdevice_nb); |
| err = nsim_dev_hwstats_event(hwstats, dev, event, ptr); |
| if (err) |
| return notifier_from_errno(err); |
| |
| return NOTIFY_OK; |
| } |
| |
| static int |
| nsim_dev_hwstats_enable_ifindex(struct nsim_dev_hwstats *hwstats, |
| int ifindex, |
| enum netdev_offload_xstats_type type, |
| struct list_head *hwsdev_list) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| struct nsim_dev *nsim_dev; |
| struct net_device *netdev; |
| bool notify = false; |
| struct net *net; |
| int err = 0; |
| |
| nsim_dev = container_of(hwstats, struct nsim_dev, hwstats); |
| net = nsim_dev_net(nsim_dev); |
| |
| rtnl_lock(); |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex); |
| if (hwsdev) |
| goto out_unlock_list; |
| |
| netdev = dev_get_by_index(net, ifindex); |
| if (!netdev) { |
| err = -ENODEV; |
| goto out_unlock_list; |
| } |
| |
| hwsdev = kzalloc(sizeof(*hwsdev), GFP_KERNEL); |
| if (!hwsdev) { |
| err = -ENOMEM; |
| goto out_put_netdev; |
| } |
| |
| hwsdev->netdev = netdev; |
| list_add_tail(&hwsdev->list, hwsdev_list); |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| |
| if (netdev_offload_xstats_enabled(netdev, type)) { |
| nsim_dev_hwsdev_enable(hwsdev, NULL); |
| notify = true; |
| } |
| |
| if (notify) |
| rtnl_offload_xstats_notify(netdev); |
| rtnl_unlock(); |
| return err; |
| |
| out_put_netdev: |
| dev_put(netdev); |
| out_unlock_list: |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| rtnl_unlock(); |
| return err; |
| } |
| |
| static int |
| nsim_dev_hwstats_disable_ifindex(struct nsim_dev_hwstats *hwstats, |
| int ifindex, |
| enum netdev_offload_xstats_type type, |
| struct list_head *hwsdev_list) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| int err = 0; |
| |
| rtnl_lock(); |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex); |
| if (hwsdev) |
| list_del(&hwsdev->list); |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| |
| if (!hwsdev) { |
| err = -ENOENT; |
| goto unlock_out; |
| } |
| |
| if (netdev_offload_xstats_enabled(hwsdev->netdev, type)) { |
| netdev_offload_xstats_push_delta(hwsdev->netdev, type, |
| &hwsdev->stats); |
| rtnl_offload_xstats_notify(hwsdev->netdev); |
| } |
| nsim_dev_hwsdev_fini(hwsdev); |
| |
| unlock_out: |
| rtnl_unlock(); |
| return err; |
| } |
| |
| static int |
| nsim_dev_hwstats_fail_ifindex(struct nsim_dev_hwstats *hwstats, |
| int ifindex, |
| enum netdev_offload_xstats_type type, |
| struct list_head *hwsdev_list) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev; |
| int err = 0; |
| |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| |
| hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex); |
| if (!hwsdev) { |
| err = -ENOENT; |
| goto err_hwsdev_list_unlock; |
| } |
| |
| hwsdev->fail_enable = true; |
| |
| err_hwsdev_list_unlock: |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| return err; |
| } |
| |
| enum nsim_dev_hwstats_do { |
| NSIM_DEV_HWSTATS_DO_DISABLE, |
| NSIM_DEV_HWSTATS_DO_ENABLE, |
| NSIM_DEV_HWSTATS_DO_FAIL, |
| }; |
| |
| struct nsim_dev_hwstats_fops { |
| const struct file_operations fops; |
| enum nsim_dev_hwstats_do action; |
| enum netdev_offload_xstats_type type; |
| }; |
| |
| static ssize_t |
| nsim_dev_hwstats_do_write(struct file *file, |
| const char __user *data, |
| size_t count, loff_t *ppos) |
| { |
| struct nsim_dev_hwstats *hwstats = file->private_data; |
| struct nsim_dev_hwstats_fops *hwsfops; |
| struct list_head *hwsdev_list; |
| int ifindex; |
| int err; |
| |
| hwsfops = container_of(debugfs_real_fops(file), |
| struct nsim_dev_hwstats_fops, fops); |
| |
| err = kstrtoint_from_user(data, count, 0, &ifindex); |
| if (err) |
| return err; |
| |
| hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, hwsfops->type); |
| if (WARN_ON(!hwsdev_list)) |
| return -EINVAL; |
| |
| switch (hwsfops->action) { |
| case NSIM_DEV_HWSTATS_DO_DISABLE: |
| err = nsim_dev_hwstats_disable_ifindex(hwstats, ifindex, |
| hwsfops->type, |
| hwsdev_list); |
| break; |
| case NSIM_DEV_HWSTATS_DO_ENABLE: |
| err = nsim_dev_hwstats_enable_ifindex(hwstats, ifindex, |
| hwsfops->type, |
| hwsdev_list); |
| break; |
| case NSIM_DEV_HWSTATS_DO_FAIL: |
| err = nsim_dev_hwstats_fail_ifindex(hwstats, ifindex, |
| hwsfops->type, |
| hwsdev_list); |
| break; |
| } |
| if (err) |
| return err; |
| |
| return count; |
| } |
| |
| #define NSIM_DEV_HWSTATS_FOPS(ACTION, TYPE) \ |
| { \ |
| .fops = { \ |
| .open = simple_open, \ |
| .write = nsim_dev_hwstats_do_write, \ |
| .llseek = generic_file_llseek, \ |
| .owner = THIS_MODULE, \ |
| }, \ |
| .action = ACTION, \ |
| .type = TYPE, \ |
| } |
| |
| static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_disable_fops = |
| NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_DISABLE, |
| NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
| |
| static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_enable_fops = |
| NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_ENABLE, |
| NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
| |
| static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_fail_fops = |
| NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_FAIL, |
| NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
| |
| #undef NSIM_DEV_HWSTATS_FOPS |
| |
| int nsim_dev_hwstats_init(struct nsim_dev *nsim_dev) |
| { |
| struct nsim_dev_hwstats *hwstats = &nsim_dev->hwstats; |
| struct net *net = nsim_dev_net(nsim_dev); |
| int err; |
| |
| mutex_init(&hwstats->hwsdev_list_lock); |
| INIT_LIST_HEAD(&hwstats->l3_list); |
| |
| hwstats->netdevice_nb.notifier_call = nsim_dev_netdevice_event; |
| err = register_netdevice_notifier_net(net, &hwstats->netdevice_nb); |
| if (err) |
| goto err_mutex_destroy; |
| |
| hwstats->ddir = debugfs_create_dir("hwstats", nsim_dev->ddir); |
| if (IS_ERR(hwstats->ddir)) { |
| err = PTR_ERR(hwstats->ddir); |
| goto err_unregister_notifier; |
| } |
| |
| hwstats->l3_ddir = debugfs_create_dir("l3", hwstats->ddir); |
| if (IS_ERR(hwstats->l3_ddir)) { |
| err = PTR_ERR(hwstats->l3_ddir); |
| goto err_remove_hwstats_recursive; |
| } |
| |
| debugfs_create_file("enable_ifindex", 0200, hwstats->l3_ddir, hwstats, |
| &nsim_dev_hwstats_l3_enable_fops.fops); |
| debugfs_create_file("disable_ifindex", 0200, hwstats->l3_ddir, hwstats, |
| &nsim_dev_hwstats_l3_disable_fops.fops); |
| debugfs_create_file("fail_next_enable", 0200, hwstats->l3_ddir, hwstats, |
| &nsim_dev_hwstats_l3_fail_fops.fops); |
| |
| INIT_DELAYED_WORK(&hwstats->traffic_dw, |
| &nsim_dev_hwstats_traffic_work); |
| schedule_delayed_work(&hwstats->traffic_dw, |
| msecs_to_jiffies(NSIM_DEV_HWSTATS_TRAFFIC_MS)); |
| return 0; |
| |
| err_remove_hwstats_recursive: |
| debugfs_remove_recursive(hwstats->ddir); |
| err_unregister_notifier: |
| unregister_netdevice_notifier_net(net, &hwstats->netdevice_nb); |
| err_mutex_destroy: |
| mutex_destroy(&hwstats->hwsdev_list_lock); |
| return err; |
| } |
| |
| static void nsim_dev_hwsdev_list_wipe(struct nsim_dev_hwstats *hwstats, |
| enum netdev_offload_xstats_type type) |
| { |
| struct nsim_dev_hwstats_netdev *hwsdev, *tmp; |
| struct list_head *hwsdev_list; |
| |
| hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type); |
| if (WARN_ON(!hwsdev_list)) |
| return; |
| |
| mutex_lock(&hwstats->hwsdev_list_lock); |
| list_for_each_entry_safe(hwsdev, tmp, hwsdev_list, list) { |
| list_del(&hwsdev->list); |
| nsim_dev_hwsdev_fini(hwsdev); |
| } |
| mutex_unlock(&hwstats->hwsdev_list_lock); |
| } |
| |
| void nsim_dev_hwstats_exit(struct nsim_dev *nsim_dev) |
| { |
| struct nsim_dev_hwstats *hwstats = &nsim_dev->hwstats; |
| struct net *net = nsim_dev_net(nsim_dev); |
| |
| cancel_delayed_work_sync(&hwstats->traffic_dw); |
| debugfs_remove_recursive(hwstats->ddir); |
| unregister_netdevice_notifier_net(net, &hwstats->netdevice_nb); |
| nsim_dev_hwsdev_list_wipe(hwstats, NETDEV_OFFLOAD_XSTATS_TYPE_L3); |
| mutex_destroy(&hwstats->hwsdev_list_lock); |
| } |