| /* |
| * teamd_link_watch.c - Team port link watchers |
| * 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 <string.h> |
| #include <unistd.h> |
| #include <errno.h> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| #include <netdb.h> |
| #include <time.h> |
| #include <limits.h> |
| #include <private/misc.h> |
| #include <team.h> |
| |
| #include "teamd.h" |
| #include "teamd_config.h" |
| #include "teamd_link_watch.h" |
| |
| extern const struct teamd_link_watch teamd_link_watch_ethtool; |
| extern const struct teamd_link_watch teamd_link_watch_arp_ping; |
| extern const struct teamd_link_watch teamd_link_watch_nsnap; |
| extern const struct teamd_link_watch teamd_link_watch_tipc; |
| |
| int __set_sockaddr(struct sockaddr *sa, socklen_t sa_len, sa_family_t family, |
| const char *hostname) |
| { |
| struct addrinfo *result; |
| struct addrinfo hints; |
| int err; |
| |
| memset(&hints, 0, sizeof(hints)); |
| hints.ai_family = family; |
| err = getaddrinfo(hostname, NULL, &hints, &result); |
| if (err) { |
| teamd_log_err("getaddrinfo failed: %s", gai_strerror(err)); |
| return -EINVAL; |
| } |
| if (sa_len != result->ai_addrlen) { |
| /* This should not happen, so just to be safe */ |
| teamd_log_err("Wrong address length in result."); |
| freeaddrinfo(result); |
| return -EINVAL; |
| } |
| memcpy(sa, result->ai_addr, sa_len); |
| freeaddrinfo(result); |
| return 0; |
| } |
| |
| |
| char *__str_sockaddr(struct sockaddr *sa, socklen_t sa_len, sa_family_t family, |
| char *buf, size_t buflen) |
| { |
| int err; |
| |
| sa->sa_family = family; |
| err = getnameinfo(sa, sa_len, buf, buflen, NULL, 0, NI_NUMERICHOST); |
| if (err) { |
| teamd_log_err("getnameinfo failed: %s", gai_strerror(err)); |
| return NULL; |
| } |
| return buf; |
| } |
| |
| int teamd_link_watch_check_link_up(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| struct lw_common_port_priv *common_ppriv, |
| bool new_link_up) |
| { |
| const char *lw_name = common_ppriv->link_watch->name; |
| |
| if (!teamd_link_watch_link_up_differs(common_ppriv, new_link_up)) |
| return 0; |
| common_ppriv->link_up = new_link_up; |
| teamd_log_info("%s: %s-link went %s.", tdport->ifname, lw_name, |
| new_link_up ? "up" : "down"); |
| if (!new_link_up && common_ppriv->link_down_count < INT_MAX) |
| common_ppriv->link_down_count++; |
| return teamd_event_port_link_changed(ctx, tdport); |
| } |
| |
| /* |
| * General link watch code |
| */ |
| static const struct teamd_link_watch *teamd_link_watch_list[] = { |
| &teamd_link_watch_ethtool, |
| &teamd_link_watch_arp_ping, |
| &teamd_link_watch_nsnap, |
| &teamd_link_watch_tipc, |
| }; |
| |
| #define TEAMD_LINK_WATCH_LIST_SIZE ARRAY_SIZE(teamd_link_watch_list) |
| |
| /* |
| * For port priv identification purposes |
| */ |
| #define LW_PORT_PRIV_CREATOR_PRIV (&teamd_link_watch_list) |
| |
| static const struct teamd_link_watch *teamd_find_link_watch(const char *link_watch_name) |
| { |
| int i; |
| |
| for (i = 0; i < TEAMD_LINK_WATCH_LIST_SIZE; i++) { |
| if (strcmp(teamd_link_watch_list[i]->name, link_watch_name) == 0) |
| return teamd_link_watch_list[i]; |
| } |
| return NULL; |
| } |
| |
| bool teamd_link_watch_port_up(struct teamd_context *ctx, |
| struct teamd_port *tdport) |
| { |
| struct lw_common_port_priv *common_ppriv; |
| bool link; |
| |
| if (!tdport) |
| return true; |
| link = true; |
| teamd_for_each_port_priv_by_creator(common_ppriv, tdport, |
| LW_PORT_PRIV_CREATOR_PRIV) { |
| link = common_ppriv->link_up; |
| if (link) |
| return link; |
| } |
| return link; |
| } |
| |
| static int teamd_link_watch_refresh_user_linkup(struct teamd_context *ctx, |
| struct teamd_port *tdport) |
| { |
| bool link; |
| bool cur_link; |
| int err; |
| |
| if (!teamd_port_present(ctx, tdport)) |
| return 0; |
| |
| link = teamd_link_watch_port_up(ctx, tdport); |
| err = team_get_port_user_linkup(ctx->th, tdport->ifindex, |
| &cur_link); |
| if (!err && link == cur_link) |
| return 0; |
| err = team_set_port_user_linkup(ctx->th, tdport->ifindex, |
| link); |
| if (err) |
| return err; |
| return 0; |
| } |
| |
| static int link_watch_state_name_get(struct teamd_context *ctx, |
| struct team_state_gsc *gsc, |
| void *priv) |
| { |
| struct lw_common_port_priv *common_ppriv = priv; |
| |
| gsc->data.str_val.ptr = common_ppriv->link_watch->name; |
| return 0; |
| } |
| |
| static int link_watch_state_up_get(struct teamd_context *ctx, |
| struct team_state_gsc *gsc, |
| void *priv) |
| { |
| struct lw_common_port_priv *common_ppriv = priv; |
| |
| gsc->data.bool_val = common_ppriv->link_up; |
| return 0; |
| } |
| |
| static int link_watch_state_down_count_get(struct teamd_context *ctx, |
| struct team_state_gsc *gsc, |
| void *priv) |
| { |
| struct lw_common_port_priv *common_ppriv = priv; |
| |
| gsc->data.int_val = common_ppriv->link_down_count; |
| return 0; |
| } |
| |
| static const struct teamd_state_val link_watch_state_vals[] = { |
| { |
| .subpath = "name", |
| .type = TEAMD_STATE_ITEM_TYPE_STRING, |
| .getter = link_watch_state_name_get, |
| }, |
| { |
| .subpath = "up", |
| .type = TEAMD_STATE_ITEM_TYPE_BOOL, |
| .getter = link_watch_state_up_get, |
| }, |
| { |
| .subpath = "down_count", |
| .type = TEAMD_STATE_ITEM_TYPE_INT, |
| .getter = link_watch_state_down_count_get, |
| }, |
| }; |
| |
| static const struct teamd_state_val link_watch_state_vg = { |
| .vals = link_watch_state_vals, |
| .vals_count = ARRAY_SIZE(link_watch_state_vals), |
| }; |
| |
| #define LW_STATE_SUBPATH "link_watches" |
| #define LW_LIST_STATE_SUBPATH LW_STATE_SUBPATH ".list" |
| |
| static int link_watch_state_register(struct teamd_context *ctx, |
| struct lw_common_port_priv *common_ppriv) |
| { |
| int err; |
| |
| err = teamd_state_val_register_ex(ctx, &link_watch_state_vg, |
| common_ppriv, common_ppriv->tdport, |
| LW_LIST_STATE_SUBPATH |
| ".link_watch_%d", |
| common_ppriv->id); |
| if (err) |
| return err; |
| |
| err = teamd_state_val_register_ex(ctx, |
| &common_ppriv->link_watch->state_vg, |
| common_ppriv, common_ppriv->tdport, |
| LW_LIST_STATE_SUBPATH |
| ".link_watch_%d", |
| common_ppriv->id); |
| if (err) |
| goto errout; |
| return 0; |
| errout: |
| teamd_state_val_unregister(ctx, &link_watch_state_vg, common_ppriv); |
| return err; |
| } |
| |
| static void link_watch_state_unregister(struct teamd_context *ctx, |
| struct lw_common_port_priv *common_ppriv) |
| { |
| teamd_state_val_unregister(ctx, &common_ppriv->link_watch->state_vg, |
| common_ppriv); |
| teamd_state_val_unregister(ctx, &link_watch_state_vg, common_ppriv); |
| } |
| |
| static unsigned int link_watch_select_free_id(struct teamd_port *tdport) |
| { |
| struct lw_common_port_priv *common_ppriv; |
| unsigned int id = 0; |
| |
| teamd_for_each_port_priv_by_creator(common_ppriv, tdport, |
| LW_PORT_PRIV_CREATOR_PRIV) { |
| if (id <= common_ppriv->id) |
| id = common_ppriv->id + 1; |
| } |
| return id; |
| } |
| |
| static int link_watch_load_config_one(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| struct teamd_config_path_cookie *cpcookie) |
| { |
| int err; |
| const char *link_watch_name; |
| const struct teamd_link_watch *link_watch; |
| unsigned int id; |
| struct lw_common_port_priv *common_ppriv; |
| bool linkup = false; |
| |
| err = team_get_port_user_linkup(ctx->th, tdport->ifindex, &linkup); |
| if (!err) { |
| teamd_log_dbg(ctx, "%s: Current user link state is \"%s\".", |
| tdport->ifname, linkup ? "up" : "down"); |
| } |
| |
| err = teamd_config_string_get(ctx, &link_watch_name, |
| "@.name", cpcookie); |
| if (err) { |
| teamd_log_err("%s: Failed to get link watch name.", |
| tdport->ifname); |
| return err; |
| } |
| link_watch = teamd_find_link_watch(link_watch_name); |
| if (!link_watch) { |
| teamd_log_err("No link_watch named \"%s\" available.", |
| link_watch_name); |
| return -EINVAL; |
| } |
| id = link_watch_select_free_id(tdport); |
| err = teamd_port_priv_create_and_get((void **) &common_ppriv, tdport, |
| &link_watch->port_priv, |
| LW_PORT_PRIV_CREATOR_PRIV); |
| if (err) |
| return err; |
| common_ppriv->id = id; |
| common_ppriv->link_watch = link_watch; |
| common_ppriv->ctx = ctx; |
| common_ppriv->tdport = tdport; |
| common_ppriv->cpcookie = cpcookie; |
| common_ppriv->link_up = linkup; |
| |
| err = link_watch_state_register(ctx, common_ppriv); |
| if (err) |
| return err; |
| return 0; |
| } |
| |
| static int link_watch_load_config(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| struct teamd_config_path_cookie *cpcookie) |
| { |
| int i; |
| int err; |
| |
| if (!teamd_config_path_is_arr(ctx, "@", cpcookie)) |
| return link_watch_load_config_one(ctx, tdport, cpcookie); |
| |
| teamd_config_for_each_arr_index(i, ctx, "@", cpcookie) { |
| struct teamd_config_path_cookie *item_cpcookie; |
| |
| item_cpcookie = teamd_config_path_cookie_get(ctx, "@[%d]", |
| cpcookie, i); |
| err = link_watch_load_config_one(ctx, tdport, item_cpcookie); |
| if (err) |
| return err; |
| } |
| return 0; |
| } |
| |
| #define TEAMD_DEFAULT_LINK_WATCH_NAME "ethtool" |
| |
| static int link_watch_event_watch_port_added(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| struct teamd_config_path_cookie *cpcookie; |
| int err; |
| |
| cpcookie = teamd_config_path_cookie_get(ctx, "$.ports.%s.link_watch", |
| tdport->ifname); |
| if (cpcookie) { |
| teamd_log_dbg(ctx, "%s: Got link watch from port config.", |
| tdport->ifname); |
| err = link_watch_load_config(ctx, tdport, cpcookie); |
| if (err) |
| return err; |
| } |
| |
| cpcookie = teamd_config_path_cookie_get(ctx, "$.link_watch"); |
| if (cpcookie) { |
| teamd_log_dbg(ctx, "%s: Got link watch from global config.", |
| tdport->ifname); |
| err = link_watch_load_config(ctx, tdport, cpcookie); |
| if (err) |
| return err; |
| } |
| |
| if (!teamd_get_first_port_priv_by_creator(tdport, |
| LW_PORT_PRIV_CREATOR_PRIV)) { |
| /* In case no link watch was found for this port, edit config |
| * by adding implicit one and call this function recursively. |
| */ |
| err = teamd_config_string_set(ctx, TEAMD_DEFAULT_LINK_WATCH_NAME, |
| "$.ports.%s.link_watch.name", |
| tdport->ifname); |
| if (err) { |
| teamd_log_err("%s: Failed to set implicit link watch name in config.", |
| tdport->ifname); |
| return err; |
| } |
| teamd_log_dbg(ctx, "%s: Using implicit link watch.", tdport->ifname); |
| return link_watch_event_watch_port_added(ctx, tdport, priv); |
| } |
| return 0; |
| } |
| |
| static void link_watch_event_watch_port_removed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| struct lw_common_port_priv *common_ppriv; |
| |
| teamd_for_each_port_priv_by_creator(common_ppriv, tdport, |
| LW_PORT_PRIV_CREATOR_PRIV) |
| link_watch_state_unregister(ctx, common_ppriv); |
| } |
| |
| static int link_watch_event_watch_port_link_changed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| return teamd_link_watch_refresh_user_linkup(ctx, tdport); |
| } |
| |
| static void __set_forced_send_for_port(struct teamd_port *tdport, |
| bool forced_send) |
| { |
| struct lw_common_port_priv *common_ppriv; |
| |
| teamd_for_each_port_priv_by_creator(common_ppriv, tdport, |
| LW_PORT_PRIV_CREATOR_PRIV) { |
| common_ppriv->forced_send = forced_send; |
| } |
| } |
| |
| static int link_watch_refresh_forced_send(struct teamd_context *ctx) |
| { |
| struct teamd_port *tdport; |
| bool port_enabled; |
| int enabled_port_count = 0; |
| int err; |
| |
| teamd_for_each_tdport(tdport, ctx) { |
| err = teamd_port_enabled_check(ctx, tdport, &port_enabled); |
| if (err) { |
| /* Looks like the options are not ready for this port. |
| * This can happen when called from |
| * link_watch_port_master_ifindex_changed(). Skip this |
| * for now, let it be handled by future call of |
| * link_watch_enabled_option_changed(). |
| */ |
| continue; |
| } |
| __set_forced_send_for_port(tdport, port_enabled); |
| if (port_enabled) |
| enabled_port_count++; |
| } |
| |
| /* |
| * In case no ports are enabled, set forced_send to true for all |
| * ports. That enforces active linkwatch approach to regain link |
| * on some port again. |
| */ |
| if (enabled_port_count == 0) { |
| teamd_for_each_tdport(tdport, ctx) |
| __set_forced_send_for_port(tdport, true); |
| } |
| return 0; |
| } |
| |
| static int link_watch_enabled_option_changed(struct teamd_context *ctx, |
| struct team_option *option, |
| void *priv) |
| { |
| return link_watch_refresh_forced_send(ctx); |
| } |
| |
| |
| static int link_watch_port_master_ifindex_changed(struct teamd_context *ctx, |
| struct teamd_port *tdport, |
| void *priv) |
| { |
| return link_watch_refresh_forced_send(ctx); |
| } |
| |
| static const struct teamd_event_watch_ops link_watch_port_watch_ops = { |
| .port_added = link_watch_event_watch_port_added, |
| .port_removed = link_watch_event_watch_port_removed, |
| .port_link_changed = link_watch_event_watch_port_link_changed, |
| .port_master_ifindex_changed = link_watch_port_master_ifindex_changed, |
| .option_changed = link_watch_enabled_option_changed, |
| .option_changed_match_name = "enabled", |
| }; |
| |
| static int port_link_state_up_get(struct teamd_context *ctx, |
| struct team_state_gsc *gsc, |
| void *priv) |
| { |
| gsc->data.bool_val = teamd_link_watch_port_up(ctx, gsc->info.tdport); |
| return 0; |
| } |
| |
| static const struct teamd_state_val link_watch_root_state_vals[] = { |
| { |
| .subpath = "up", |
| .type = TEAMD_STATE_ITEM_TYPE_BOOL, |
| .getter = port_link_state_up_get, |
| }, |
| }; |
| |
| static const struct teamd_state_val link_watch_root_state_vg = { |
| .subpath = LW_STATE_SUBPATH, |
| .vals = link_watch_root_state_vals, |
| .vals_count = ARRAY_SIZE(link_watch_root_state_vals), |
| .per_port = true, |
| }; |
| |
| int teamd_link_watch_init(struct teamd_context *ctx) |
| { |
| int err; |
| |
| err = teamd_event_watch_register(ctx, &link_watch_port_watch_ops, NULL); |
| if (err) { |
| teamd_log_err("Failed to register event watch."); |
| return err; |
| } |
| err = teamd_state_val_register(ctx, &link_watch_root_state_vg, ctx); |
| if (err) |
| goto event_watch_unregister; |
| return 0; |
| |
| event_watch_unregister: |
| teamd_event_watch_unregister(ctx, &link_watch_port_watch_ops, NULL); |
| return err; |
| } |
| |
| void teamd_link_watch_fini(struct teamd_context *ctx) |
| { |
| teamd_state_val_unregister(ctx, &link_watch_root_state_vg, ctx); |
| teamd_event_watch_unregister(ctx, &link_watch_port_watch_ops, NULL); |
| } |