blob: 35b39a38a90bf154945ba43b28722da12983a2db [file] [log] [blame]
/*
* 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,
};