| /* |
| * teamd_runner_activebackup.c - Active-backup runners |
| * Copyright (C) 2012-2015 Jiri Pirko <jiri@resnulli.us> |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| */ |
| |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <errno.h> |
| #include <string.h> |
| #include <sys/socket.h> |
| #include <linux/netdevice.h> |
| #include <limits.h> |
| #include <private/misc.h> |
| #include <team.h> |
| |
| #include "teamd.h" |
| #include "teamd_config.h" |
| #include "teamd_state.h" |
| #include "teamd_workq.h" |
| |
| struct ab; |
| |
| struct ab_hwaddr_policy { |
| const char *name; |
| int (*hwaddr_changed)(struct teamd_context *ctx, |
| struct ab *ab); |
| int (*port_hwaddr_changed)(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *tdport); |
| int (*port_added)(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *tdport); |
| int (*active_set)(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *tdport); |
| int (*active_clear)(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *tdport); |
| }; |
| |
| struct ab { |
| uint32_t active_ifindex; |
| char active_orig_hwaddr[MAX_ADDR_LEN]; |
| const struct ab_hwaddr_policy *hwaddr_policy; |
| struct teamd_workq link_watch_handler_workq; |
| }; |
| |
| struct ab_port { |
| struct teamd_port *tdport; |
| struct { |
| bool sticky; |
| #define AB_DFLT_PORT_STICKY false |
| } cfg; |
| }; |
| |
| static struct ab_port *ab_port_get(struct ab *ab, struct teamd_port *tdport) |
| { |
| /* |
| * When calling this after teamd_event_watch_register() which is in |
| * ab_init() it is ensured that this will always return valid priv |
| * pointer for an existing port. |
| */ |
| return teamd_get_first_port_priv_by_creator(tdport, ab); |
| } |
| |
| static bool ab_is_port_sticky(struct ab *ab, struct teamd_port *tdport) |
| { |
| return ab_port_get(ab, tdport)->cfg.sticky; |
| } |
| |
| static int ab_hwaddr_policy_same_all_hwaddr_changed(struct teamd_context *ctx, |
| struct ab *ab) |
| { |
| struct teamd_port *tdport; |
| int err; |
| |
| teamd_for_each_tdport(tdport, ctx) { |
| err = team_hwaddr_set(ctx->th, tdport->ifindex, ctx->hwaddr, |
| ctx->hwaddr_len); |
| if (err) { |
| teamd_log_err("%s: Failed to set port hardware address.", |
| tdport->ifname); |
| return err; |
| } |
| } |
| return 0; |
| } |
| |
| static int |
| ab_hwaddr_policy_same_all_port_hwaddr_changed(struct teamd_context *ctx, |
| struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| if (!memcmp(team_get_ifinfo_hwaddr(tdport->team_ifinfo), |
| ctx->hwaddr, ctx->hwaddr_len)) |
| return 0; |
| |
| err = team_hwaddr_set(ctx->th, tdport->ifindex, ctx->hwaddr, |
| ctx->hwaddr_len); |
| if (err) |
| teamd_log_err("%s: Failed to set port hardware address.", |
| tdport->ifname); |
| |
| return err; |
| } |
| |
| static int ab_hwaddr_policy_same_all_port_added(struct teamd_context *ctx, |
| struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| err = team_hwaddr_set(ctx->th, tdport->ifindex, ctx->hwaddr, |
| ctx->hwaddr_len); |
| if (err) { |
| teamd_log_err("%s: Failed to set port hardware address.", |
| tdport->ifname); |
| return err; |
| } |
| return 0; |
| } |
| |
| static const struct ab_hwaddr_policy ab_hwaddr_policy_same_all = { |
| .name = "same_all", |
| .hwaddr_changed = ab_hwaddr_policy_same_all_hwaddr_changed, |
| .port_hwaddr_changed = ab_hwaddr_policy_same_all_port_hwaddr_changed, |
| .port_added = ab_hwaddr_policy_same_all_port_added, |
| }; |
| |
| static int ab_hwaddr_policy_by_active_active_set(struct teamd_context *ctx, |
| struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| err = team_hwaddr_set(ctx->th, ctx->ifindex, |
| team_get_ifinfo_hwaddr(tdport->team_ifinfo), |
| ctx->hwaddr_len); |
| if (err) { |
| teamd_log_err("Failed to set team hardware address."); |
| return err; |
| } |
| return 0; |
| } |
| |
| static const struct ab_hwaddr_policy ab_hwaddr_policy_by_active = { |
| .name = "by_active", |
| .active_set = ab_hwaddr_policy_by_active_active_set, |
| }; |
| |
| static int ab_hwaddr_policy_only_active_hwaddr_changed(struct teamd_context *ctx, |
| struct ab *ab) |
| { |
| struct teamd_port *tdport; |
| int err; |
| |
| tdport = teamd_get_port(ctx, ab->active_ifindex); |
| if (!tdport || !teamd_port_present(ctx, tdport)) |
| return 0; |
| err = team_hwaddr_set(ctx->th, tdport->ifindex, ctx->hwaddr, |
| ctx->hwaddr_len); |
| if (err) { |
| teamd_log_err("%s: Failed to set port hardware address.", |
| tdport->ifname); |
| return err; |
| } |
| return 0; |
| } |
| |
| static int ab_hwaddr_policy_only_active_active_set(struct teamd_context *ctx, |
| struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| memcpy(ab->active_orig_hwaddr, |
| team_get_ifinfo_hwaddr(tdport->team_ifinfo), |
| ctx->hwaddr_len); |
| err = team_hwaddr_set(ctx->th, tdport->ifindex, ctx->hwaddr, |
| ctx->hwaddr_len); |
| if (err) { |
| teamd_log_err("%s: Failed to set port hardware address.", |
| tdport->ifname); |
| return err; |
| } |
| return 0; |
| } |
| |
| static int ab_hwaddr_policy_only_active_active_clear(struct teamd_context *ctx, |
| struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| err = team_hwaddr_set(ctx->th, tdport->ifindex, |
| ab->active_orig_hwaddr, |
| ctx->hwaddr_len); |
| if (err) { |
| teamd_log_err("%s: Failed to set port hardware address.", |
| tdport->ifname); |
| return err; |
| } |
| return 0; |
| } |
| |
| static const struct ab_hwaddr_policy ab_hwaddr_policy_only_active = { |
| .name = "only_active", |
| .hwaddr_changed = ab_hwaddr_policy_only_active_hwaddr_changed, |
| .active_set = ab_hwaddr_policy_only_active_active_set, |
| .active_clear = ab_hwaddr_policy_only_active_active_clear, |
| }; |
| |
| static const struct ab_hwaddr_policy *ab_hwaddr_policy_list[] = { |
| &ab_hwaddr_policy_same_all, |
| &ab_hwaddr_policy_by_active, |
| &ab_hwaddr_policy_only_active, |
| }; |
| |
| #define AB_HWADDR_POLICY_LIST_SIZE ARRAY_SIZE(ab_hwaddr_policy_list) |
| |
| static int ab_assign_hwaddr_policy(struct ab *ab, |
| const char *hwaddr_policy_name) |
| { |
| int i = 0; |
| |
| if (!hwaddr_policy_name) |
| goto found; |
| for (i = 0; i < AB_HWADDR_POLICY_LIST_SIZE; i++) |
| if (!strcmp(ab_hwaddr_policy_list[i]->name, hwaddr_policy_name)) |
| goto found; |
| return -ENOENT; |
| found: |
| ab->hwaddr_policy = ab_hwaddr_policy_list[i]; |
| return 0; |
| } |
| |
| static int ab_clear_active_port(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| ab->active_ifindex = 0; |
| if (!tdport || !teamd_port_present(ctx, tdport)) |
| return 0; |
| teamd_log_dbg(ctx, "Clearing active port \"%s\".", tdport->ifname); |
| |
| err = team_set_port_enabled(ctx->th, tdport->ifindex, false); |
| if (err) { |
| teamd_log_err("%s: Failed to disable active port.", |
| tdport->ifname); |
| return err; |
| } |
| if (ab->hwaddr_policy->active_clear) { |
| err = ab->hwaddr_policy->active_clear(ctx, ab, tdport); |
| if (err) |
| return err; |
| } |
| return 0; |
| } |
| |
| static int ab_set_active_port(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *tdport) |
| { |
| int err; |
| |
| err = team_set_port_enabled(ctx->th, tdport->ifindex, true); |
| if (err) { |
| teamd_log_err("%s: Failed to enable active port.", |
| tdport->ifname); |
| return err; |
| } |
| err = team_set_active_port(ctx->th, tdport->ifindex); |
| if (err) { |
| teamd_log_err("%s: Failed to set as active port.", |
| tdport->ifname); |
| goto err_set_active_port; |
| } |
| if (ab->hwaddr_policy->active_set) { |
| err = ab->hwaddr_policy->active_set(ctx, ab, tdport); |
| if (err) |
| goto err_hwaddr_policy_active_set; |
| } |
| ab->active_ifindex = tdport->ifindex; |
| teamd_log_info("Changed active port to \"%s\".", tdport->ifname); |
| return 0; |
| |
| err_set_active_port: |
| err_hwaddr_policy_active_set: |
| team_set_port_enabled(ctx->th, tdport->ifindex, false); |
| return err; |
| } |
| |
| struct ab_port_state_info { |
| struct teamd_port *tdport; |
| uint32_t speed; |
| uint8_t duplex; |
| int prio; |
| }; |
| |
| static void ab_best_port_check_set(struct teamd_context *ctx, |
| struct ab_port_state_info *best, |
| struct teamd_port *tdport) |
| { |
| struct team_port *port = tdport->team_port; |
| uint32_t speed; |
| uint8_t duplex; |
| int prio; |
| |
| if (!teamd_link_watch_port_up(ctx, tdport) || best->tdport == tdport) |
| return; |
| |
| speed = team_get_port_speed(port); |
| duplex = team_get_port_duplex(port); |
| prio = teamd_port_prio(ctx, tdport); |
| |
| if (!best->tdport || (prio > best->prio) || (speed > best->speed) || |
| (speed == best->speed && duplex > best->duplex)) { |
| best->tdport = tdport; |
| best->prio = prio; |
| best->speed = speed; |
| best->duplex = duplex; |
| } |
| } |
| |
| static int ab_change_active_port(struct teamd_context *ctx, struct ab *ab, |
| struct teamd_port *active_tdport, |
| struct teamd_port *new_active_tdport) |
| { |
| int err; |
| |
| err = ab_clear_active_port(ctx, ab, active_tdport); |
| if (err && !TEAMD_ENOENT(err)) |
| return err; |
| err = ab_set_active_port(ctx, ab, new_active_tdport); |
| if (err) { |
| if (TEAMD_ENOENT(err)) |
| /* Queue another best port selection */ |
| teamd_workq_schedule_work(ctx, &ab->link_watch_handler_workq); |
| else |
| return err; |
| } |
| return 0; |
| } |
| |
| static int ab_link_watch_handler(struct teamd_context *ctx, struct ab *ab) |
| { |
| struct teamd_port *tdport; |
| struct teamd_port *active_tdport; |
| struct ab_port_state_info best; |
| int err; |
| uint32_t active_ifindex; |
| |
| memset(&best, 0, sizeof(best)); |
| best.prio = INT_MIN; |
| |
| active_tdport = teamd_get_port(ctx, ab->active_ifindex); |
| if (active_tdport) { |
| teamd_log_dbg(ctx, "Current active port: \"%s\" (ifindex \"%d\", prio \"%d\").", |
| active_tdport->ifname, active_tdport->ifindex, |
| teamd_port_prio(ctx, active_tdport)); |
| |
| err = team_get_active_port(ctx->th, &active_ifindex); |
| if (err) { |
| teamd_log_err("Failed to get active port."); |
| return err; |
| } |
| |
| /* |
| * When active port went down or it is other than currently set, |
| * clear it and proceed as if none was set in the first place. |
| */ |
| if (!teamd_link_watch_port_up(ctx, active_tdport) || |
| active_ifindex != active_tdport->ifindex) { |
| err = ab_clear_active_port(ctx, ab, active_tdport); |
| if (err) |
| return err; |
| active_tdport = NULL; |
| } |
| } |
| |
| /* |
| * Find the best port amond all ports. Prefer the currently active |
| * port, if there's any. This is because other port might have the |
| * same prio, speed and duplex. We do not want to change in that case |
| */ |
| if (active_tdport && teamd_port_present(ctx, active_tdport)) |
| ab_best_port_check_set(ctx, &best, active_tdport); |
| teamd_for_each_tdport(tdport, ctx) |
| ab_best_port_check_set(ctx, &best, tdport); |
| |
| if (!best.tdport || best.tdport == active_tdport) |
| return 0; |
| |
| teamd_log_dbg(ctx, "Found best port: \"%s\" (ifindex \"%d\", prio \"%d\").", |
| best.tdport->ifname, best.tdport->ifindex, best.prio); |
| |
| if (!active_tdport || !ab_is_port_sticky(ab, active_tdport)) { |
| err = ab_change_active_port(ctx, ab, active_tdport, |
| best.tdport); |
| if (err) |
| return err; |
| } |
| return 0; |
| } |
| |
| static int ab_link_watch_handler_work(struct teamd_context *ctx, |
| struct teamd_workq *workq) |
| { |
| struct ab *ab; |
| |
| ab = get_container(workq, struct ab, link_watch_handler_workq); |
| return ab_link_watch_handler(ctx, ab); |
| } |
| |
| static int ab_event_watch_hwaddr_changed(struct teamd_context *ctx, void *priv) |
| { |
| struct ab *ab = priv; |
| |
| if (ab->hwaddr_policy->hwaddr_changed) |
| return ab->hwaddr_policy->hwaddr_changed(ctx, ab); |
| return 0; |
| } |
| |
| static int ab_event_watch_port_hwaddr_changed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| struct ab *ab = priv; |
| |
| if (!teamd_port_present(ctx, tdport)) |
| return 0; |
| |
| if (ab->hwaddr_policy->port_hwaddr_changed) |
| return ab->hwaddr_policy->port_hwaddr_changed(ctx, ab, tdport); |
| |
| return 0; |
| } |
| |
| static int ab_port_load_config(struct teamd_context *ctx, |
| struct ab_port *ab_port) |
| { |
| const char *port_name = ab_port->tdport->ifname; |
| int err; |
| |
| err = teamd_config_bool_get(ctx, &ab_port->cfg.sticky, |
| "$.ports.%s.sticky", port_name); |
| if (err) |
| ab_port->cfg.sticky = AB_DFLT_PORT_STICKY; |
| teamd_log_dbg(ctx, "%s: Using sticky \"%d\".", port_name, |
| ab_port->cfg.sticky); |
| return 0; |
| } |
| |
| static int ab_port_added(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv, void *creator_priv) |
| { |
| struct ab_port *ab_port = priv; |
| struct ab *ab = creator_priv; |
| int err; |
| |
| ab_port->tdport = tdport; |
| err = ab_port_load_config(ctx, ab_port); |
| if (err) { |
| teamd_log_err("Failed to load port config."); |
| return err; |
| } |
| /* Newly added ports are disabled */ |
| err = team_set_port_enabled(ctx->th, tdport->ifindex, false); |
| if (err) { |
| teamd_log_err("%s: Failed to disable port.", tdport->ifname); |
| return TEAMD_ENOENT(err) ? 0 : err; |
| } |
| |
| if (ab->hwaddr_policy->port_added) |
| return ab->hwaddr_policy->port_added(ctx, ab, tdport); |
| return 0; |
| } |
| |
| static void ab_port_removed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv, void *creator_priv) |
| { |
| struct ab *ab = creator_priv; |
| |
| ab_link_watch_handler(ctx, ab); |
| } |
| |
| static const struct teamd_port_priv ab_port_priv = { |
| .init = ab_port_added, |
| .fini = ab_port_removed, |
| .priv_size = sizeof(struct ab_port), |
| }; |
| |
| static int ab_event_watch_port_added(struct teamd_context *ctx, |
| struct teamd_port *tdport, void *priv) |
| { |
| struct ab *ab = priv; |
| |
| return teamd_port_priv_create(tdport, &ab_port_priv, ab); |
| } |
| |
| static int ab_event_watch_port_link_changed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| return ab_link_watch_handler(ctx, priv); |
| } |
| |
| static int ab_event_watch_port_master_ifindex_changed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| return ab_link_watch_handler(ctx, priv); |
| } |
| |
| static int ab_event_watch_prio_option_changed(struct teamd_context *ctx, |
| struct team_option *option, |
| void *priv) |
| { |
| return ab_link_watch_handler(ctx, priv); |
| } |
| |
| static const struct teamd_event_watch_ops ab_event_watch_ops = { |
| .hwaddr_changed = ab_event_watch_hwaddr_changed, |
| .port_hwaddr_changed = ab_event_watch_port_hwaddr_changed, |
| .port_added = ab_event_watch_port_added, |
| .port_link_changed = ab_event_watch_port_link_changed, |
| .port_master_ifindex_changed = ab_event_watch_port_master_ifindex_changed, |
| .option_changed = ab_event_watch_prio_option_changed, |
| .option_changed_match_name = "priority", |
| }; |
| |
| static int ab_load_config(struct teamd_context *ctx, struct ab *ab) |
| { |
| int err; |
| const char *hwaddr_policy_name; |
| |
| err = teamd_config_string_get(ctx, &hwaddr_policy_name, "$.runner.hwaddr_policy"); |
| if (err) |
| hwaddr_policy_name = NULL; |
| err = ab_assign_hwaddr_policy(ab, hwaddr_policy_name); |
| if (err) { |
| teamd_log_err("Unknown \"hwaddr_policy\" named \"%s\" passed.", |
| hwaddr_policy_name); |
| return err; |
| } |
| teamd_log_dbg(ctx, "Using hwaddr_policy \"%s\".", ab->hwaddr_policy->name); |
| return 0; |
| } |
| |
| static int ab_state_active_port_get(struct teamd_context *ctx, |
| struct team_state_gsc *gsc, |
| void *priv) |
| { |
| struct ab *ab = priv; |
| struct teamd_port *active_tdport; |
| |
| active_tdport = teamd_get_port(ctx, ab->active_ifindex); |
| gsc->data.str_val.ptr = active_tdport ? active_tdport->ifname : ""; |
| return 0; |
| } |
| |
| struct ab_active_port_set_info { |
| struct teamd_workq workq; |
| struct ab *ab; |
| uint32_t ifindex; |
| }; |
| |
| static int ab_active_port_set_work(struct teamd_context *ctx, |
| struct teamd_workq *workq) |
| { |
| struct ab_active_port_set_info *info; |
| struct ab *ab; |
| uint32_t ifindex; |
| struct teamd_port *tdport; |
| struct teamd_port *active_tdport; |
| |
| info = get_container(workq, struct ab_active_port_set_info, workq); |
| ab = info->ab; |
| ifindex = info->ifindex; |
| free(info); |
| tdport = teamd_get_port(ctx, ifindex); |
| if (!tdport) |
| /* Port disapeared in between, ignore */ |
| return 0; |
| active_tdport = teamd_get_port(ctx, ab->active_ifindex); |
| return ab_change_active_port(ctx, ab, active_tdport, tdport); |
| } |
| |
| static int ab_state_active_port_set(struct teamd_context *ctx, |
| struct team_state_gsc *gsc, |
| void *priv) |
| { |
| struct ab_active_port_set_info *info; |
| struct ab *ab = priv; |
| struct teamd_port *tdport; |
| |
| tdport = teamd_get_port_by_ifname(ctx, (const char *) gsc->data.str_val.ptr); |
| if (!tdport) |
| return -ENODEV; |
| info = malloc(sizeof(*info)); |
| if (!info) |
| return -ENOMEM; |
| teamd_workq_init_work(&info->workq, ab_active_port_set_work); |
| info->ab = ab; |
| info->ifindex = tdport->ifindex; |
| teamd_workq_schedule_work(ctx, &info->workq); |
| return 0; |
| } |
| |
| static const struct teamd_state_val ab_state_vals[] = { |
| { |
| .subpath = "active_port", |
| .type = TEAMD_STATE_ITEM_TYPE_STRING, |
| .getter = ab_state_active_port_get, |
| .setter = ab_state_active_port_set, |
| }, |
| }; |
| |
| static const struct teamd_state_val ab_state_vg = { |
| .subpath = "runner", |
| .vals = ab_state_vals, |
| .vals_count = ARRAY_SIZE(ab_state_vals), |
| }; |
| |
| static int ab_init(struct teamd_context *ctx, void *priv) |
| { |
| struct ab *ab = priv; |
| int err; |
| |
| if (!teamd_config_path_exists(ctx, "$.notify_peers.count")) { |
| err = teamd_config_int_set(ctx, 1, "$.notify_peers.count"); |
| if (err) { |
| teamd_log_err("Failed to set notify_peers count config value."); |
| return err; |
| } |
| } |
| if (!teamd_config_path_exists(ctx, "$.mcast_rejoin.count")) { |
| err = teamd_config_int_set(ctx, 1, "$.mcast_rejoin.count"); |
| if (err) { |
| teamd_log_err("Failed to set mcast_rejoin count config value."); |
| return err; |
| } |
| } |
| err = ab_load_config(ctx, ab); |
| if (err) { |
| teamd_log_err("Failed to load config values."); |
| return err; |
| } |
| err = teamd_event_watch_register(ctx, &ab_event_watch_ops, ab); |
| if (err) { |
| teamd_log_err("Failed to register event watch."); |
| return err; |
| } |
| err = teamd_state_val_register(ctx, &ab_state_vg, ab); |
| if (err) { |
| teamd_log_err("Failed to register state value group."); |
| goto event_watch_unregister; |
| } |
| teamd_workq_init_work(&ab->link_watch_handler_workq, |
| ab_link_watch_handler_work); |
| return 0; |
| |
| event_watch_unregister: |
| teamd_event_watch_unregister(ctx, &ab_event_watch_ops, ab); |
| return err; |
| } |
| |
| static void ab_fini(struct teamd_context *ctx, void *priv) |
| { |
| struct ab *ab = priv; |
| |
| teamd_state_val_unregister(ctx, &ab_state_vg, ab); |
| teamd_event_watch_unregister(ctx, &ab_event_watch_ops, ab); |
| } |
| |
| const struct teamd_runner teamd_runner_activebackup = { |
| .name = "activebackup", |
| .team_mode_name = "activebackup", |
| .priv_size = sizeof(struct ab), |
| .init = ab_init, |
| .fini = ab_fini, |
| }; |