| /* | |
| * drivers/net/team/team_mode_loadbalance.c - Load-balancing mode for team | |
| * Copyright (c) 2012 Jiri Pirko <jpirko@redhat.com> | |
| * | |
| * This program is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU General Public License as published by | |
| * the Free Software Foundation; either version 2 of the License, or | |
| * (at your option) any later version. | |
| */ | |
| #include <linux/kernel.h> | |
| #include <linux/types.h> | |
| #include <linux/module.h> | |
| #include <linux/init.h> | |
| #include <linux/errno.h> | |
| #include <linux/netdevice.h> | |
| #include <linux/filter.h> | |
| #include <linux/if_team.h> | |
| struct lb_priv; | |
| typedef struct team_port *lb_select_tx_port_func_t(struct team *, | |
| struct lb_priv *, | |
| struct sk_buff *, | |
| unsigned char); | |
| #define LB_TX_HASHTABLE_SIZE 256 /* hash is a char */ | |
| struct lb_stats { | |
| u64 tx_bytes; | |
| }; | |
| struct lb_pcpu_stats { | |
| struct lb_stats hash_stats[LB_TX_HASHTABLE_SIZE]; | |
| struct u64_stats_sync syncp; | |
| }; | |
| struct lb_stats_info { | |
| struct lb_stats stats; | |
| struct lb_stats last_stats; | |
| struct team_option_inst_info *opt_inst_info; | |
| }; | |
| struct lb_port_mapping { | |
| struct team_port __rcu *port; | |
| struct team_option_inst_info *opt_inst_info; | |
| }; | |
| struct lb_priv_ex { | |
| struct team *team; | |
| struct lb_port_mapping tx_hash_to_port_mapping[LB_TX_HASHTABLE_SIZE]; | |
| struct sock_fprog_kern *orig_fprog; | |
| struct { | |
| unsigned int refresh_interval; /* in tenths of second */ | |
| struct delayed_work refresh_dw; | |
| struct lb_stats_info info[LB_TX_HASHTABLE_SIZE]; | |
| } stats; | |
| }; | |
| struct lb_priv { | |
| struct bpf_prog __rcu *fp; | |
| lb_select_tx_port_func_t __rcu *select_tx_port_func; | |
| struct lb_pcpu_stats __percpu *pcpu_stats; | |
| struct lb_priv_ex *ex; /* priv extension */ | |
| }; | |
| static struct lb_priv *get_lb_priv(struct team *team) | |
| { | |
| return (struct lb_priv *) &team->mode_priv; | |
| } | |
| struct lb_port_priv { | |
| struct lb_stats __percpu *pcpu_stats; | |
| struct lb_stats_info stats_info; | |
| }; | |
| static struct lb_port_priv *get_lb_port_priv(struct team_port *port) | |
| { | |
| return (struct lb_port_priv *) &port->mode_priv; | |
| } | |
| #define LB_HTPM_PORT_BY_HASH(lp_priv, hash) \ | |
| (lb_priv)->ex->tx_hash_to_port_mapping[hash].port | |
| #define LB_HTPM_OPT_INST_INFO_BY_HASH(lp_priv, hash) \ | |
| (lb_priv)->ex->tx_hash_to_port_mapping[hash].opt_inst_info | |
| static void lb_tx_hash_to_port_mapping_null_port(struct team *team, | |
| struct team_port *port) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| bool changed = false; | |
| int i; | |
| for (i = 0; i < LB_TX_HASHTABLE_SIZE; i++) { | |
| struct lb_port_mapping *pm; | |
| pm = &lb_priv->ex->tx_hash_to_port_mapping[i]; | |
| if (rcu_access_pointer(pm->port) == port) { | |
| RCU_INIT_POINTER(pm->port, NULL); | |
| team_option_inst_set_change(pm->opt_inst_info); | |
| changed = true; | |
| } | |
| } | |
| if (changed) | |
| team_options_change_check(team); | |
| } | |
| /* Basic tx selection based solely by hash */ | |
| static struct team_port *lb_hash_select_tx_port(struct team *team, | |
| struct lb_priv *lb_priv, | |
| struct sk_buff *skb, | |
| unsigned char hash) | |
| { | |
| int port_index = team_num_to_port_index(team, hash); | |
| return team_get_port_by_index_rcu(team, port_index); | |
| } | |
| /* Hash to port mapping select tx port */ | |
| static struct team_port *lb_htpm_select_tx_port(struct team *team, | |
| struct lb_priv *lb_priv, | |
| struct sk_buff *skb, | |
| unsigned char hash) | |
| { | |
| return rcu_dereference_bh(LB_HTPM_PORT_BY_HASH(lb_priv, hash)); | |
| } | |
| struct lb_select_tx_port { | |
| char *name; | |
| lb_select_tx_port_func_t *func; | |
| }; | |
| static const struct lb_select_tx_port lb_select_tx_port_list[] = { | |
| { | |
| .name = "hash", | |
| .func = lb_hash_select_tx_port, | |
| }, | |
| { | |
| .name = "hash_to_port_mapping", | |
| .func = lb_htpm_select_tx_port, | |
| }, | |
| }; | |
| #define LB_SELECT_TX_PORT_LIST_COUNT ARRAY_SIZE(lb_select_tx_port_list) | |
| static char *lb_select_tx_port_get_name(lb_select_tx_port_func_t *func) | |
| { | |
| int i; | |
| for (i = 0; i < LB_SELECT_TX_PORT_LIST_COUNT; i++) { | |
| const struct lb_select_tx_port *item; | |
| item = &lb_select_tx_port_list[i]; | |
| if (item->func == func) | |
| return item->name; | |
| } | |
| return NULL; | |
| } | |
| static lb_select_tx_port_func_t *lb_select_tx_port_get_func(const char *name) | |
| { | |
| int i; | |
| for (i = 0; i < LB_SELECT_TX_PORT_LIST_COUNT; i++) { | |
| const struct lb_select_tx_port *item; | |
| item = &lb_select_tx_port_list[i]; | |
| if (!strcmp(item->name, name)) | |
| return item->func; | |
| } | |
| return NULL; | |
| } | |
| static unsigned int lb_get_skb_hash(struct lb_priv *lb_priv, | |
| struct sk_buff *skb) | |
| { | |
| struct bpf_prog *fp; | |
| uint32_t lhash; | |
| unsigned char *c; | |
| fp = rcu_dereference_bh(lb_priv->fp); | |
| if (unlikely(!fp)) | |
| return 0; | |
| lhash = BPF_PROG_RUN(fp, skb); | |
| c = (char *) &lhash; | |
| return c[0] ^ c[1] ^ c[2] ^ c[3]; | |
| } | |
| static void lb_update_tx_stats(unsigned int tx_bytes, struct lb_priv *lb_priv, | |
| struct lb_port_priv *lb_port_priv, | |
| unsigned char hash) | |
| { | |
| struct lb_pcpu_stats *pcpu_stats; | |
| struct lb_stats *port_stats; | |
| struct lb_stats *hash_stats; | |
| pcpu_stats = this_cpu_ptr(lb_priv->pcpu_stats); | |
| port_stats = this_cpu_ptr(lb_port_priv->pcpu_stats); | |
| hash_stats = &pcpu_stats->hash_stats[hash]; | |
| u64_stats_update_begin(&pcpu_stats->syncp); | |
| port_stats->tx_bytes += tx_bytes; | |
| hash_stats->tx_bytes += tx_bytes; | |
| u64_stats_update_end(&pcpu_stats->syncp); | |
| } | |
| static bool lb_transmit(struct team *team, struct sk_buff *skb) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| lb_select_tx_port_func_t *select_tx_port_func; | |
| struct team_port *port; | |
| unsigned char hash; | |
| unsigned int tx_bytes = skb->len; | |
| hash = lb_get_skb_hash(lb_priv, skb); | |
| select_tx_port_func = rcu_dereference_bh(lb_priv->select_tx_port_func); | |
| port = select_tx_port_func(team, lb_priv, skb, hash); | |
| if (unlikely(!port)) | |
| goto drop; | |
| if (team_dev_queue_xmit(team, port, skb)) | |
| return false; | |
| lb_update_tx_stats(tx_bytes, lb_priv, get_lb_port_priv(port), hash); | |
| return true; | |
| drop: | |
| dev_kfree_skb_any(skb); | |
| return false; | |
| } | |
| static int lb_bpf_func_get(struct team *team, struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| if (!lb_priv->ex->orig_fprog) { | |
| ctx->data.bin_val.len = 0; | |
| ctx->data.bin_val.ptr = NULL; | |
| return 0; | |
| } | |
| ctx->data.bin_val.len = lb_priv->ex->orig_fprog->len * | |
| sizeof(struct sock_filter); | |
| ctx->data.bin_val.ptr = lb_priv->ex->orig_fprog->filter; | |
| return 0; | |
| } | |
| static int __fprog_create(struct sock_fprog_kern **pfprog, u32 data_len, | |
| const void *data) | |
| { | |
| struct sock_fprog_kern *fprog; | |
| struct sock_filter *filter = (struct sock_filter *) data; | |
| if (data_len % sizeof(struct sock_filter)) | |
| return -EINVAL; | |
| fprog = kmalloc(sizeof(*fprog), GFP_KERNEL); | |
| if (!fprog) | |
| return -ENOMEM; | |
| fprog->filter = kmemdup(filter, data_len, GFP_KERNEL); | |
| if (!fprog->filter) { | |
| kfree(fprog); | |
| return -ENOMEM; | |
| } | |
| fprog->len = data_len / sizeof(struct sock_filter); | |
| *pfprog = fprog; | |
| return 0; | |
| } | |
| static void __fprog_destroy(struct sock_fprog_kern *fprog) | |
| { | |
| kfree(fprog->filter); | |
| kfree(fprog); | |
| } | |
| static int lb_bpf_func_set(struct team *team, struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| struct bpf_prog *fp = NULL; | |
| struct bpf_prog *orig_fp = NULL; | |
| struct sock_fprog_kern *fprog = NULL; | |
| int err; | |
| if (ctx->data.bin_val.len) { | |
| err = __fprog_create(&fprog, ctx->data.bin_val.len, | |
| ctx->data.bin_val.ptr); | |
| if (err) | |
| return err; | |
| err = bpf_prog_create(&fp, fprog); | |
| if (err) { | |
| __fprog_destroy(fprog); | |
| return err; | |
| } | |
| } | |
| if (lb_priv->ex->orig_fprog) { | |
| /* Clear old filter data */ | |
| __fprog_destroy(lb_priv->ex->orig_fprog); | |
| orig_fp = rcu_dereference_protected(lb_priv->fp, | |
| lockdep_is_held(&team->lock)); | |
| } | |
| rcu_assign_pointer(lb_priv->fp, fp); | |
| lb_priv->ex->orig_fprog = fprog; | |
| if (orig_fp) { | |
| synchronize_rcu(); | |
| bpf_prog_destroy(orig_fp); | |
| } | |
| return 0; | |
| } | |
| static int lb_tx_method_get(struct team *team, struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| lb_select_tx_port_func_t *func; | |
| char *name; | |
| func = rcu_dereference_protected(lb_priv->select_tx_port_func, | |
| lockdep_is_held(&team->lock)); | |
| name = lb_select_tx_port_get_name(func); | |
| BUG_ON(!name); | |
| ctx->data.str_val = name; | |
| return 0; | |
| } | |
| static int lb_tx_method_set(struct team *team, struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| lb_select_tx_port_func_t *func; | |
| func = lb_select_tx_port_get_func(ctx->data.str_val); | |
| if (!func) | |
| return -EINVAL; | |
| rcu_assign_pointer(lb_priv->select_tx_port_func, func); | |
| return 0; | |
| } | |
| static int lb_tx_hash_to_port_mapping_init(struct team *team, | |
| struct team_option_inst_info *info) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| unsigned char hash = info->array_index; | |
| LB_HTPM_OPT_INST_INFO_BY_HASH(lb_priv, hash) = info; | |
| return 0; | |
| } | |
| static int lb_tx_hash_to_port_mapping_get(struct team *team, | |
| struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| struct team_port *port; | |
| unsigned char hash = ctx->info->array_index; | |
| port = LB_HTPM_PORT_BY_HASH(lb_priv, hash); | |
| ctx->data.u32_val = port ? port->dev->ifindex : 0; | |
| return 0; | |
| } | |
| static int lb_tx_hash_to_port_mapping_set(struct team *team, | |
| struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| struct team_port *port; | |
| unsigned char hash = ctx->info->array_index; | |
| list_for_each_entry(port, &team->port_list, list) { | |
| if (ctx->data.u32_val == port->dev->ifindex && | |
| team_port_enabled(port)) { | |
| rcu_assign_pointer(LB_HTPM_PORT_BY_HASH(lb_priv, hash), | |
| port); | |
| return 0; | |
| } | |
| } | |
| return -ENODEV; | |
| } | |
| static int lb_hash_stats_init(struct team *team, | |
| struct team_option_inst_info *info) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| unsigned char hash = info->array_index; | |
| lb_priv->ex->stats.info[hash].opt_inst_info = info; | |
| return 0; | |
| } | |
| static int lb_hash_stats_get(struct team *team, struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| unsigned char hash = ctx->info->array_index; | |
| ctx->data.bin_val.ptr = &lb_priv->ex->stats.info[hash].stats; | |
| ctx->data.bin_val.len = sizeof(struct lb_stats); | |
| return 0; | |
| } | |
| static int lb_port_stats_init(struct team *team, | |
| struct team_option_inst_info *info) | |
| { | |
| struct team_port *port = info->port; | |
| struct lb_port_priv *lb_port_priv = get_lb_port_priv(port); | |
| lb_port_priv->stats_info.opt_inst_info = info; | |
| return 0; | |
| } | |
| static int lb_port_stats_get(struct team *team, struct team_gsetter_ctx *ctx) | |
| { | |
| struct team_port *port = ctx->info->port; | |
| struct lb_port_priv *lb_port_priv = get_lb_port_priv(port); | |
| ctx->data.bin_val.ptr = &lb_port_priv->stats_info.stats; | |
| ctx->data.bin_val.len = sizeof(struct lb_stats); | |
| return 0; | |
| } | |
| static void __lb_stats_info_refresh_prepare(struct lb_stats_info *s_info) | |
| { | |
| memcpy(&s_info->last_stats, &s_info->stats, sizeof(struct lb_stats)); | |
| memset(&s_info->stats, 0, sizeof(struct lb_stats)); | |
| } | |
| static bool __lb_stats_info_refresh_check(struct lb_stats_info *s_info, | |
| struct team *team) | |
| { | |
| if (memcmp(&s_info->last_stats, &s_info->stats, | |
| sizeof(struct lb_stats))) { | |
| team_option_inst_set_change(s_info->opt_inst_info); | |
| return true; | |
| } | |
| return false; | |
| } | |
| static void __lb_one_cpu_stats_add(struct lb_stats *acc_stats, | |
| struct lb_stats *cpu_stats, | |
| struct u64_stats_sync *syncp) | |
| { | |
| unsigned int start; | |
| struct lb_stats tmp; | |
| do { | |
| start = u64_stats_fetch_begin_irq(syncp); | |
| tmp.tx_bytes = cpu_stats->tx_bytes; | |
| } while (u64_stats_fetch_retry_irq(syncp, start)); | |
| acc_stats->tx_bytes += tmp.tx_bytes; | |
| } | |
| static void lb_stats_refresh(struct work_struct *work) | |
| { | |
| struct team *team; | |
| struct lb_priv *lb_priv; | |
| struct lb_priv_ex *lb_priv_ex; | |
| struct lb_pcpu_stats *pcpu_stats; | |
| struct lb_stats *stats; | |
| struct lb_stats_info *s_info; | |
| struct team_port *port; | |
| bool changed = false; | |
| int i; | |
| int j; | |
| lb_priv_ex = container_of(work, struct lb_priv_ex, | |
| stats.refresh_dw.work); | |
| team = lb_priv_ex->team; | |
| lb_priv = get_lb_priv(team); | |
| if (!mutex_trylock(&team->lock)) { | |
| schedule_delayed_work(&lb_priv_ex->stats.refresh_dw, 0); | |
| return; | |
| } | |
| for (j = 0; j < LB_TX_HASHTABLE_SIZE; j++) { | |
| s_info = &lb_priv->ex->stats.info[j]; | |
| __lb_stats_info_refresh_prepare(s_info); | |
| for_each_possible_cpu(i) { | |
| pcpu_stats = per_cpu_ptr(lb_priv->pcpu_stats, i); | |
| stats = &pcpu_stats->hash_stats[j]; | |
| __lb_one_cpu_stats_add(&s_info->stats, stats, | |
| &pcpu_stats->syncp); | |
| } | |
| changed |= __lb_stats_info_refresh_check(s_info, team); | |
| } | |
| list_for_each_entry(port, &team->port_list, list) { | |
| struct lb_port_priv *lb_port_priv = get_lb_port_priv(port); | |
| s_info = &lb_port_priv->stats_info; | |
| __lb_stats_info_refresh_prepare(s_info); | |
| for_each_possible_cpu(i) { | |
| pcpu_stats = per_cpu_ptr(lb_priv->pcpu_stats, i); | |
| stats = per_cpu_ptr(lb_port_priv->pcpu_stats, i); | |
| __lb_one_cpu_stats_add(&s_info->stats, stats, | |
| &pcpu_stats->syncp); | |
| } | |
| changed |= __lb_stats_info_refresh_check(s_info, team); | |
| } | |
| if (changed) | |
| team_options_change_check(team); | |
| schedule_delayed_work(&lb_priv_ex->stats.refresh_dw, | |
| (lb_priv_ex->stats.refresh_interval * HZ) / 10); | |
| mutex_unlock(&team->lock); | |
| } | |
| static int lb_stats_refresh_interval_get(struct team *team, | |
| struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| ctx->data.u32_val = lb_priv->ex->stats.refresh_interval; | |
| return 0; | |
| } | |
| static int lb_stats_refresh_interval_set(struct team *team, | |
| struct team_gsetter_ctx *ctx) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| unsigned int interval; | |
| interval = ctx->data.u32_val; | |
| if (lb_priv->ex->stats.refresh_interval == interval) | |
| return 0; | |
| lb_priv->ex->stats.refresh_interval = interval; | |
| if (interval) | |
| schedule_delayed_work(&lb_priv->ex->stats.refresh_dw, 0); | |
| else | |
| cancel_delayed_work(&lb_priv->ex->stats.refresh_dw); | |
| return 0; | |
| } | |
| static const struct team_option lb_options[] = { | |
| { | |
| .name = "bpf_hash_func", | |
| .type = TEAM_OPTION_TYPE_BINARY, | |
| .getter = lb_bpf_func_get, | |
| .setter = lb_bpf_func_set, | |
| }, | |
| { | |
| .name = "lb_tx_method", | |
| .type = TEAM_OPTION_TYPE_STRING, | |
| .getter = lb_tx_method_get, | |
| .setter = lb_tx_method_set, | |
| }, | |
| { | |
| .name = "lb_tx_hash_to_port_mapping", | |
| .array_size = LB_TX_HASHTABLE_SIZE, | |
| .type = TEAM_OPTION_TYPE_U32, | |
| .init = lb_tx_hash_to_port_mapping_init, | |
| .getter = lb_tx_hash_to_port_mapping_get, | |
| .setter = lb_tx_hash_to_port_mapping_set, | |
| }, | |
| { | |
| .name = "lb_hash_stats", | |
| .array_size = LB_TX_HASHTABLE_SIZE, | |
| .type = TEAM_OPTION_TYPE_BINARY, | |
| .init = lb_hash_stats_init, | |
| .getter = lb_hash_stats_get, | |
| }, | |
| { | |
| .name = "lb_port_stats", | |
| .per_port = true, | |
| .type = TEAM_OPTION_TYPE_BINARY, | |
| .init = lb_port_stats_init, | |
| .getter = lb_port_stats_get, | |
| }, | |
| { | |
| .name = "lb_stats_refresh_interval", | |
| .type = TEAM_OPTION_TYPE_U32, | |
| .getter = lb_stats_refresh_interval_get, | |
| .setter = lb_stats_refresh_interval_set, | |
| }, | |
| }; | |
| static int lb_init(struct team *team) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| lb_select_tx_port_func_t *func; | |
| int i, err; | |
| /* set default tx port selector */ | |
| func = lb_select_tx_port_get_func("hash"); | |
| BUG_ON(!func); | |
| rcu_assign_pointer(lb_priv->select_tx_port_func, func); | |
| lb_priv->ex = kzalloc(sizeof(*lb_priv->ex), GFP_KERNEL); | |
| if (!lb_priv->ex) | |
| return -ENOMEM; | |
| lb_priv->ex->team = team; | |
| lb_priv->pcpu_stats = alloc_percpu(struct lb_pcpu_stats); | |
| if (!lb_priv->pcpu_stats) { | |
| err = -ENOMEM; | |
| goto err_alloc_pcpu_stats; | |
| } | |
| for_each_possible_cpu(i) { | |
| struct lb_pcpu_stats *team_lb_stats; | |
| team_lb_stats = per_cpu_ptr(lb_priv->pcpu_stats, i); | |
| u64_stats_init(&team_lb_stats->syncp); | |
| } | |
| INIT_DELAYED_WORK(&lb_priv->ex->stats.refresh_dw, lb_stats_refresh); | |
| err = team_options_register(team, lb_options, ARRAY_SIZE(lb_options)); | |
| if (err) | |
| goto err_options_register; | |
| return 0; | |
| err_options_register: | |
| free_percpu(lb_priv->pcpu_stats); | |
| err_alloc_pcpu_stats: | |
| kfree(lb_priv->ex); | |
| return err; | |
| } | |
| static void lb_exit(struct team *team) | |
| { | |
| struct lb_priv *lb_priv = get_lb_priv(team); | |
| team_options_unregister(team, lb_options, | |
| ARRAY_SIZE(lb_options)); | |
| cancel_delayed_work_sync(&lb_priv->ex->stats.refresh_dw); | |
| free_percpu(lb_priv->pcpu_stats); | |
| kfree(lb_priv->ex); | |
| } | |
| static int lb_port_enter(struct team *team, struct team_port *port) | |
| { | |
| struct lb_port_priv *lb_port_priv = get_lb_port_priv(port); | |
| lb_port_priv->pcpu_stats = alloc_percpu(struct lb_stats); | |
| if (!lb_port_priv->pcpu_stats) | |
| return -ENOMEM; | |
| return 0; | |
| } | |
| static void lb_port_leave(struct team *team, struct team_port *port) | |
| { | |
| struct lb_port_priv *lb_port_priv = get_lb_port_priv(port); | |
| free_percpu(lb_port_priv->pcpu_stats); | |
| } | |
| static void lb_port_disabled(struct team *team, struct team_port *port) | |
| { | |
| lb_tx_hash_to_port_mapping_null_port(team, port); | |
| } | |
| static const struct team_mode_ops lb_mode_ops = { | |
| .init = lb_init, | |
| .exit = lb_exit, | |
| .port_enter = lb_port_enter, | |
| .port_leave = lb_port_leave, | |
| .port_disabled = lb_port_disabled, | |
| .transmit = lb_transmit, | |
| }; | |
| static const struct team_mode lb_mode = { | |
| .kind = "loadbalance", | |
| .owner = THIS_MODULE, | |
| .priv_size = sizeof(struct lb_priv), | |
| .port_priv_size = sizeof(struct lb_port_priv), | |
| .ops = &lb_mode_ops, | |
| }; | |
| static int __init lb_init_module(void) | |
| { | |
| return team_mode_register(&lb_mode); | |
| } | |
| static void __exit lb_cleanup_module(void) | |
| { | |
| team_mode_unregister(&lb_mode); | |
| } | |
| module_init(lb_init_module); | |
| module_exit(lb_cleanup_module); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_AUTHOR("Jiri Pirko <jpirko@redhat.com>"); | |
| MODULE_DESCRIPTION("Load-balancing mode for team"); | |
| MODULE_ALIAS("team-mode-loadbalance"); |