/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2024 Red Hat, Inc.
 */

#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"

#include "nm-devlink.h"

#include <linux/if.h>
#include <linux/devlink.h>

#include "libnm-log-core/nm-logging.h"
#include "libnm-platform/nm-netlink.h"
#include "libnm-platform/nm-platform.h"
#include "libnm-platform/nm-platform-utils.h"
#include "libnm-platform/nmp-ethtool-ioctl.h"

#define _NMLOG_PREFIX_NAME "devlink"
#define _NMLOG_DOMAIN      LOGD_PLATFORM | LOGD_DEVICE
#define _NMLOG(level, ...)                                                                        \
    G_STMT_START                                                                                  \
    {                                                                                             \
        char        _ifname_buf[IFNAMSIZ];                                                        \
        const char *_ifname = self ? nmp_utils_if_indextoname(self->ifindex, _ifname_buf) : NULL; \
                                                                                                  \
        nm_log((level),                                                                           \
               _NMLOG_DOMAIN,                                                                     \
               _ifname ?: NULL,                                                                   \
               NULL,                                                                              \
               "%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                   \
               _NMLOG_PREFIX_NAME,                                                                \
               NM_PRINT_FMT_QUOTED(_ifname, " (", _ifname, ")", "")                               \
                   _NM_UTILS_MACRO_REST(__VA_ARGS__));                                            \
    }                                                                                             \
    G_STMT_END

#define CB_RESULT_PENDING 0
#define CB_RESULT_OK      1

struct _NMDevlink {
    NMPlatform     *plat;
    struct nl_sock *genl_sock_sync;
    guint16         genl_family_id;
    int             ifindex;
};

/**
 * nm_devlink_new:
 * @platform: the #NMPlatform that will use this #NMDevlink instance
 * @genl_sock_sync: the netlink socket (will be used synchronously)
 * @ifindex: the kernel's netdev ifindex corresponding to the devlink device
 *
 * Create a new #NMDevlink instance to make devlink queries regarding a specific
 * device.
 *
 * Returns: (transfer full): the allocated new #NMDevlink
 */
NMDevlink *
nm_devlink_new(NMPlatform *platform, struct nl_sock *genl_sock_sync, int ifindex)
{
    NMDevlink *self = g_new(NMDevlink, 1);

    self->plat           = platform;
    self->genl_sock_sync = genl_sock_sync;
    self->genl_family_id = nm_platform_genl_get_family_id(platform, NMP_GENL_FAMILY_TYPE_DEVLINK);
    self->ifindex        = ifindex;
    return self;
}

/**
 * nm_devlink_get_dev_identifier:
 * @self: the #NMDevlink
 * @out_bus: (out): the "bus_name" part of the devlink device identifier
 * @out_addr: (out): the "bus_addr" part of the devlink device identifier
 * @error: (optional): the error location
 *
 * Get the devlink device identifier of the device for which the #NMDevlink was
 * created (with the @ifindex argument of nm_devlink_get_new()). A devlink device
 * is identified as "bus_name/bus_addr" (i.e. "pci/0000:65:00.0"). This function
 * provides both parts separately.
 *
 * Note that here we only get the potential devlink device identifier. The real devlink
 * device might not even exist if the hw doesn't implement devlink or the netdev
 * doesn't have a 1-1 corresponding devlink device (i.e. because it's a VF or
 * because the hw uses a "one eswitch for many ports" model).
 *
 * Also note that currently only PCI devices are supported, an error will be
 * returned for other kind of devices.
 *
 * Returns: FALSE in case of error, TRUE otherwise
 */
gboolean
nm_devlink_get_dev_identifier(NMDevlink *self, char **out_bus, char **out_addr, GError **error)
{
    const char               *bus;
    char                      sbuf[IFNAMSIZ];
    NMPUtilsEthtoolDriverInfo ethtool_driver_info;

    nm_assert(out_bus != NULL && out_addr != NULL);
    nm_assert(!error || !*error);

    if (!nm_platform_link_get_udev_property(self->plat, self->ifindex, "ID_BUS", &bus)) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "Can't get udev info for device '%s'",
                    nmp_utils_if_indextoname(self->ifindex, sbuf));
        return FALSE;
    }

    if (!nm_streq0(bus, "pci")) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "Devlink is only supported for PCI but device '%s' has bus name '%s'",
                    nmp_utils_if_indextoname(self->ifindex, sbuf),
                    bus);
        return FALSE;
    }

    if (!nmp_ethtool_ioctl_get_driver_info(self->ifindex, &ethtool_driver_info)) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "Can't get ethtool driver info for device '%s'",
                    nmp_utils_if_indextoname(self->ifindex, sbuf));
        return FALSE;
    }

    *out_bus  = g_strdup("pci");
    *out_addr = g_strdup(ethtool_driver_info._private_bus_info);
    return TRUE;
}

static struct nl_msg *
devlink_alloc_msg(NMDevlink *self, uint8_t cmd, uint16_t flags)
{
    nm_auto_nlmsg struct nl_msg *msg = nlmsg_alloc(0);
    if (!msg)
        return NULL;

    genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, self->genl_family_id, 0, flags, cmd, 0);
    return g_steal_pointer(&msg);
}

static int
ack_cb_handler(const struct nl_msg *msg, void *data)
{
    int *result = data;
    *result     = CB_RESULT_OK;
    return NL_STOP;
}

static int
finish_cb_handler(const struct nl_msg *msg, void *data)
{
    int *result = data;
    *result     = CB_RESULT_OK;
    return NL_SKIP;
}

static int
err_cb_handler(const struct sockaddr_nl *nla, const struct nlmsgerr *err, void *data)
{
    void      **args       = data;
    NMDevlink  *self       = args[0];
    int        *result     = args[1];
    char      **err_msg    = args[2];
    const char *extack_msg = NULL;

    *result = err->error;
    nlmsg_parse_error(nlmsg_undata(err), &extack_msg);

    _LOGT("error response (%d - %s)", err->error, extack_msg ?: nm_strerror(err->error));

    if (err_msg)
        *err_msg = g_strdup(extack_msg ?: nm_strerror(err->error));

    return NL_SKIP;
}

static int
devlink_send_and_recv(NMDevlink     *self,
                      struct nl_msg *msg,
                      int (*valid_handler)(const struct nl_msg *, void *),
                      void  *valid_data,
                      char **err_msg)
{
    int                nle;
    int                cb_result = CB_RESULT_PENDING;
    void              *err_arg[] = {self, &cb_result, err_msg};
    const struct nl_cb cb        = {
               .err_cb     = err_cb_handler,
               .err_arg    = err_arg,
               .finish_cb  = finish_cb_handler,
               .finish_arg = &cb_result,
               .ack_cb     = ack_cb_handler,
               .ack_arg    = &cb_result,
               .valid_cb   = valid_handler,
               .valid_arg  = valid_data,
    };

    g_return_val_if_fail(msg != NULL, -ENOMEM);

    if (err_msg)
        *err_msg = NULL;

    nle = nl_send_auto(self->genl_sock_sync, msg);
    if (nle < 0)
        goto out;

    while (cb_result == CB_RESULT_PENDING) {
        nle = nl_recvmsgs(self->genl_sock_sync, &cb);
        if (nle < 0 && nle != -EAGAIN) {
            _LOGW("nl_recvmsgs() error (%d - %s)", nle, nm_strerror(nle));
            break;
        }
    }

out:
    if (nle < 0 && err_msg && *err_msg == NULL)
        *err_msg = strdup(nm_strerror(nle));

    if (nle >= 0 && cb_result < 0)
        nle = cb_result;
    return nle;
}

static int
devlink_parse_eswitch_mode(const struct nl_msg *msg, void *data)
{
    static const struct nla_policy eswitch_policy[] = {
        [DEVLINK_ATTR_ESWITCH_MODE]        = {.type = NLA_U16},
        [DEVLINK_ATTR_ESWITCH_INLINE_MODE] = {.type = NLA_U8},
        [DEVLINK_ATTR_ESWITCH_ENCAP_MODE]  = {.type = NLA_U8},
    };
    NMDevlinkEswitchParams *params = data;
    struct genlmsghdr      *gnlh   = nlmsg_data(nlmsg_hdr(msg));
    struct nlattr          *tb[G_N_ELEMENTS(eswitch_policy)];
    struct nlattr          *nla;

    if (nla_parse_arr(tb, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), eswitch_policy) < 0)
        return NL_SKIP;

    nla          = tb[DEVLINK_ATTR_ESWITCH_MODE];
    params->mode = nla ? (_NMSriovEswitchMode) nla_get_u16(nla) : _NM_SRIOV_ESWITCH_MODE_UNKNOWN;

    nla = tb[DEVLINK_ATTR_ESWITCH_INLINE_MODE];
    params->inline_mode =
        nla ? (_NMSriovEswitchInlineMode) nla_get_u8(nla) : _NM_SRIOV_ESWITCH_INLINE_MODE_UNKNOWN;

    nla = tb[DEVLINK_ATTR_ESWITCH_ENCAP_MODE];
    params->encap_mode =
        nla ? (_NMSriovEswitchEncapMode) nla_get_u8(nla) : _NM_SRIOV_ESWITCH_ENCAP_MODE_UNKNOWN;

    return NL_OK;
}

/*
 * nm_devlink_get_eswitch_params:
 * @self: the #NMDevlink
 * @out_params: the eswitch parameters read via Devlink
 * @error: the error location
 *
 * Get the eswitch configuration of the device related to the #NMDevlink instance. Note
 * that this might be unsupported by the device (see nm_devlink_get_dev()).
 *
 * Returns: FALSE in case of error, TRUE otherwise
 */
gboolean
nm_devlink_get_eswitch_params(NMDevlink *self, NMDevlinkEswitchParams *out_params, GError **error)
{
    nm_auto_nlmsg struct nl_msg *msg     = NULL;
    gs_free char                *bus     = NULL;
    gs_free char                *addr    = NULL;
    gs_free char                *err_msg = NULL;
    int                          rc;

    nm_assert(out_params);

    if (!nm_devlink_get_dev_identifier(self, &bus, &addr, error))
        return FALSE;

    msg = devlink_alloc_msg(self, DEVLINK_CMD_ESWITCH_GET, 0);
    NLA_PUT_STRING(msg, DEVLINK_ATTR_BUS_NAME, bus);
    NLA_PUT_STRING(msg, DEVLINK_ATTR_DEV_NAME, addr);

    rc = devlink_send_and_recv(self, msg, devlink_parse_eswitch_mode, out_params, &err_msg);
    if (rc < 0) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "devlink: eswitch get failed (%d - %s)",
                    rc,
                    err_msg);
        return FALSE;
    }

    _LOGD("eswitch get success");

    return TRUE;

nla_put_failure:
    g_return_val_if_reached(FALSE);
}

/*
 * nm_devlink_set_eswitch_params:
 * @self: the #NMDevlink
 * @params: the eswitch parameters to set
 * @error: the error location
 *
 * Set the eswitch configuration of the device related to the #NMDevlink instance. Note
 * that this might be unsupported by the device (see nm_devlink_get_dev()).
 *
 * If any of the eswitch parameters is set to "preserve" it won't be modified.
 *
 * Returns: FALSE in case of error, TRUE otherwise
 */
gboolean
nm_devlink_set_eswitch_params(NMDevlink *self, NMDevlinkEswitchParams params, GError **error)
{
    nm_auto_nlmsg struct nl_msg *msg     = NULL;
    gs_free char                *bus     = NULL;
    gs_free char                *addr    = NULL;
    gs_free char                *err_msg = NULL;
    int                          rc;

    if (params.mode == _NM_SRIOV_ESWITCH_MODE_PRESERVE
        && params.inline_mode == _NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE
        && params.encap_mode == _NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE)
        return TRUE;

    if (!nm_devlink_get_dev_identifier(self, &bus, &addr, error))
        return FALSE;

    msg = devlink_alloc_msg(self, DEVLINK_CMD_ESWITCH_SET, 0);
    NLA_PUT_STRING(msg, DEVLINK_ATTR_BUS_NAME, bus);
    NLA_PUT_STRING(msg, DEVLINK_ATTR_DEV_NAME, addr);

    if (params.mode != _NM_SRIOV_ESWITCH_MODE_PRESERVE)
        NLA_PUT_U16(msg, DEVLINK_ATTR_ESWITCH_MODE, params.mode);
    if (params.inline_mode != _NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE)
        NLA_PUT_U8(msg, DEVLINK_ATTR_ESWITCH_INLINE_MODE, params.inline_mode);
    if (params.encap_mode != _NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE)
        NLA_PUT_U8(msg, DEVLINK_ATTR_ESWITCH_ENCAP_MODE, params.encap_mode);

    rc = devlink_send_and_recv(self, msg, NULL, NULL, &err_msg);
    if (rc < 0) {
        g_set_error(error,
                    NM_UTILS_ERROR,
                    NM_UTILS_ERROR_UNKNOWN,
                    "devlink: eswitch set failed (%d - %s)",
                    rc,
                    err_msg);
        return FALSE;
    }

    _LOGD("eswitch set success");

    return TRUE;

nla_put_failure:
    g_return_val_if_reached(FALSE);
}
