/* SPDX-License-Identifier: LGPL-2.1+
 * Copyright © 2020 VMware, Inc. */

#include "alloc-util.h"
#include "netlink-util.h"
#include "networkd-manager.h"
#include "networkd-sriov.h"
#include "parse-util.h"
#include "set.h"
#include "string-util.h"

static int sr_iov_new(SRIOV **ret) {
        SRIOV *sr_iov;

        sr_iov = new(SRIOV, 1);
        if (!sr_iov)
                return -ENOMEM;

        *sr_iov = (SRIOV) {
                  .vf = (uint32_t) -1,
                  .vlan_proto = ETH_P_8021Q,
                  .vf_spoof_check_setting = -1,
                  .trust = -1,
                  .query_rss = -1,
                  .link_state = _SR_IOV_LINK_STATE_INVALID,
        };

        *ret = TAKE_PTR(sr_iov);

        return 0;
}

static int sr_iov_new_static(Network *network, const char *filename, unsigned section_line, SRIOV **ret) {
        _cleanup_(network_config_section_freep) NetworkConfigSection *n = NULL;
        _cleanup_(sr_iov_freep) SRIOV *sr_iov = NULL;
        SRIOV *existing = NULL;
        int r;

        assert(network);
        assert(ret);
        assert(filename);
        assert(section_line > 0);

        r = network_config_section_new(filename, section_line, &n);
        if (r < 0)
                return r;

        existing = ordered_hashmap_get(network->sr_iov_by_section, n);
        if (existing) {
                *ret = existing;
                return 0;
        }

        r = sr_iov_new(&sr_iov);
        if (r < 0)
                return r;

        sr_iov->network = network;
        sr_iov->section = TAKE_PTR(n);

        r = ordered_hashmap_ensure_allocated(&network->sr_iov_by_section, &network_config_hash_ops);
        if (r < 0)
                return r;

        r = ordered_hashmap_put(network->sr_iov_by_section, sr_iov->section, sr_iov);
        if (r < 0)
                return r;

        *ret = TAKE_PTR(sr_iov);
        return 0;
}

SRIOV *sr_iov_free(SRIOV *sr_iov) {
        if (!sr_iov)
                return NULL;

        if (sr_iov->network && sr_iov->section)
                ordered_hashmap_remove(sr_iov->network->sr_iov_by_section, sr_iov->section);

        network_config_section_free(sr_iov->section);

        return mfree(sr_iov);
}

static int sr_iov_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
        int r;

        assert(link);
        assert(link->sr_iov_messages > 0);
        link->sr_iov_messages--;

        if (IN_SET(link->state, LINK_STATE_FAILED, LINK_STATE_LINGER))
                return 1;

        r = sd_netlink_message_get_errno(m);
        if (r < 0 && r != -EEXIST) {
                log_link_message_error_errno(link, m, r, "Could not set up SR-IOV");
                link_enter_failed(link);
                return 1;
        }

        if (link->sr_iov_messages == 0) {
                log_link_debug(link, "SR-IOV configured");
                link->sr_iov_configured = true;
                link_check_ready(link);
        }

        return 1;
}

static int sr_iov_configure(Link *link, SRIOV *sr_iov) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
        int r;

        assert(link);
        assert(link->manager);
        assert(link->manager->rtnl);
        assert(link->ifindex > 0);

        log_link_debug(link, "Setting SR-IOV virtual function %"PRIu32, sr_iov->vf);

        r = sd_rtnl_message_new_link(link->manager->rtnl, &req, RTM_SETLINK, link->ifindex);
        if (r < 0)
                return log_link_error_errno(link, r, "Could not allocate RTM_SETLINK message: %m");

        r = sd_netlink_message_open_container(req, IFLA_VFINFO_LIST);
        if (r < 0)
                return log_link_error_errno(link, r, "Could not open IFLA_VFINFO_LIST container: %m");

        r = sd_netlink_message_open_container(req, IFLA_VF_INFO);
        if (r < 0)
                return log_link_error_errno(link, r, "Could not open IFLA_VF_INFO container: %m");

        if (!ether_addr_is_null(&sr_iov->mac)) {
                struct ifla_vf_mac ivm = {
                        .vf = sr_iov->vf,
                };

                memcpy(ivm.mac, &sr_iov->mac, ETH_ALEN);
                r = sd_netlink_message_append_data(req, IFLA_VF_MAC, &ivm, sizeof(struct ifla_vf_mac));
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not append IFLA_VF_MAC: %m");
        }

        if (sr_iov->vf_spoof_check_setting >= 0) {
                struct ifla_vf_spoofchk ivs = {
                        .vf = sr_iov->vf,
                        .setting = sr_iov->vf_spoof_check_setting,
                };

                r = sd_netlink_message_append_data(req, IFLA_VF_SPOOFCHK, &ivs, sizeof(struct ifla_vf_spoofchk));
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not append IFLA_VF_SPOOFCHK: %m");
        }

        if (sr_iov->query_rss >= 0) {
                struct ifla_vf_rss_query_en ivs = {
                        .vf = sr_iov->vf,
                        .setting = sr_iov->query_rss,
                };

                r = sd_netlink_message_append_data(req, IFLA_VF_RSS_QUERY_EN, &ivs, sizeof(struct ifla_vf_rss_query_en));
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not append IFLA_VF_RSS_QUERY_EN: %m");
        }

        if (sr_iov->trust >= 0) {
                struct ifla_vf_trust ivt = {
                        .vf = sr_iov->vf,
                        .setting = sr_iov->trust,
                };

                r = sd_netlink_message_append_data(req, IFLA_VF_TRUST, &ivt, sizeof(struct ifla_vf_trust));
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not append IFLA_VF_TRUST: %m");
        }

        if (sr_iov->link_state >= 0) {
                struct ifla_vf_link_state ivl = {
                        .vf = sr_iov->vf,
                        .link_state = sr_iov->link_state,
                };

                r = sd_netlink_message_append_data(req, IFLA_VF_LINK_STATE, &ivl, sizeof(struct ifla_vf_link_state));
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not append IFLA_VF_LINK_STATE: %m");
        }

        if (sr_iov->vlan > 0) {
                /* Because of padding, first the buffer must be initialized with 0. */
                struct ifla_vf_vlan_info ivvi = {};
                ivvi.vf = sr_iov->vf;
                ivvi.vlan = sr_iov->vlan;
                ivvi.qos = sr_iov->qos;
                ivvi.vlan_proto = htobe16(sr_iov->vlan_proto);

                r = sd_netlink_message_open_container(req, IFLA_VF_VLAN_LIST);
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not open IFLA_VF_VLAN_LIST container: %m");

                r = sd_netlink_message_append_data(req, IFLA_VF_VLAN_INFO, &ivvi, sizeof(struct ifla_vf_vlan_info));
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not append IFLA_VF_VLAN_INFO: %m");

                r = sd_netlink_message_close_container(req);
                if (r < 0)
                        return log_link_error_errno(link, r, "Could not close IFLA_VF_VLAN_LIST container: %m");
        }

        r = sd_netlink_message_close_container(req);
        if (r < 0)
                return log_link_error_errno(link, r, "Could not close IFLA_VF_INFO container: %m");

        r = sd_netlink_message_close_container(req);
        if (r < 0)
                return log_link_error_errno(link, r, "Could not close IFLA_VFINFO_LIST container: %m");

        r = netlink_call_async(link->manager->rtnl, NULL, req, sr_iov_handler,
                               link_netlink_destroy_callback, link);
        if (r < 0)
                return log_link_error_errno(link, r, "Could not send rtnetlink message: %m");

        link_ref(link);
        link->sr_iov_messages++;

        return 0;
}

int link_configure_sr_iov(Link *link) {
        SRIOV *sr_iov;
        int r;

        link->sr_iov_configured = false;
        link->sr_iov_messages = 0;

        ORDERED_HASHMAP_FOREACH(sr_iov, link->network->sr_iov_by_section) {
                r = sr_iov_configure(link, sr_iov);
                if (r < 0)
                        return r;
        }

        if (link->sr_iov_messages == 0)
                link->sr_iov_configured = true;
        else
                log_link_debug(link, "Configuring SR-IOV");

        return 0;
}

static int sr_iov_section_verify(SRIOV *sr_iov) {
        assert(sr_iov);

        if (section_is_invalid(sr_iov->section))
                return -EINVAL;

        if (sr_iov->vf == (uint32_t) -1)
                return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
                                         "%s: [SRIOV] section without VirtualFunction= field configured. "
                                         "Ignoring [SRIOV] section from line %u.",
                                         sr_iov->section->filename, sr_iov->section->line);

        return 0;
}

void network_drop_invalid_sr_iov(Network *network) {
        SRIOV *sr_iov;

        assert(network);

        ORDERED_HASHMAP_FOREACH(sr_iov, network->sr_iov_by_section)
                if (sr_iov_section_verify(sr_iov) < 0)
                        sr_iov_free(sr_iov);
}

int config_parse_sr_iov_uint32(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
        Network *network = data;
        uint32_t k;
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        r = sr_iov_new_static(network, filename, section_line, &sr_iov);
        if (r < 0)
                return r;

        if (isempty(rvalue)) {
                if (streq(lvalue, "VirtualFunction"))
                        sr_iov->vf = (uint32_t) -1;
                else if (streq(lvalue, "VLANId"))
                        sr_iov->vlan = 0;
                else if (streq(lvalue, "QualityOfService"))
                        sr_iov->qos = 0;
                else
                        assert_not_reached("Invalid lvalue");

                TAKE_PTR(sr_iov);
                return 0;
        }

        r = safe_atou32(rvalue, &k);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
                return 0;
        }

        if (streq(lvalue, "VLANId")) {
                if (k == 0 || k > 4095) {
                        log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid SR-IOV VLANId: %d", k);
                        return 0;
                }
                sr_iov->vlan = k;
        } else if (streq(lvalue, "VirtualFunction")) {
                if (k >= INT_MAX) {
                        log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid SR-IOV virtual function: %d", k);
                        return 0;
                }
                sr_iov->vf = k;
        } else if (streq(lvalue, "QualityOfService"))
                sr_iov->qos = k;
        else
                assert_not_reached("Invalid lvalue");

        TAKE_PTR(sr_iov);
        return 0;
}

int config_parse_sr_iov_vlan_proto(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
        Network *network = data;
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        r = sr_iov_new_static(network, filename, section_line, &sr_iov);
        if (r < 0)
                return r;

        if (isempty(rvalue) || streq(rvalue, "802.1Q"))
                sr_iov->vlan_proto = ETH_P_8021Q;
        else if (streq(rvalue, "802.1ad"))
                sr_iov->vlan_proto = ETH_P_8021AD;
        else {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Invalid SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
                return 0;
        }

        TAKE_PTR(sr_iov);
        return 0;
}

int config_parse_sr_iov_link_state(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
        Network *network = data;
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        r = sr_iov_new_static(network, filename, section_line, &sr_iov);
        if (r < 0)
                return r;

        /* Unfortunately, SR_IOV_LINK_STATE_DISABLE is 2, not 0. So, we cannot use
         * DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN() macro. */

        if (isempty(rvalue)) {
                sr_iov->link_state = _SR_IOV_LINK_STATE_INVALID;
                TAKE_PTR(sr_iov);
                return 0;
        }

        if (streq(rvalue, "auto")) {
                sr_iov->link_state = SR_IOV_LINK_STATE_AUTO;
                TAKE_PTR(sr_iov);
                return 0;
        }

        r = parse_boolean(rvalue);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
                return 0;
        }

        sr_iov->link_state = r ? SR_IOV_LINK_STATE_ENABLE : SR_IOV_LINK_STATE_DISABLE;
        TAKE_PTR(sr_iov);
        return 0;
}

int config_parse_sr_iov_boolean(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
        Network *network = data;
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        r = sr_iov_new_static(network, filename, section_line, &sr_iov);
        if (r < 0)
                return r;

        if (isempty(rvalue)) {
                if (streq(lvalue, "MACSpoofCheck"))
                        sr_iov->vf_spoof_check_setting = -1;
                else if (streq(lvalue, "QueryReceiveSideScaling"))
                        sr_iov->query_rss = -1;
                else if (streq(lvalue, "Trust"))
                        sr_iov->trust = -1;
                else
                        assert_not_reached("Invalid lvalue");

                TAKE_PTR(sr_iov);
                return 0;
        }

        r = parse_boolean(rvalue);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse '%s=', ignoring: %s", lvalue, rvalue);
                return 0;
        }

        if (streq(lvalue, "MACSpoofCheck"))
                sr_iov->vf_spoof_check_setting = r;
        else if (streq(lvalue, "QueryReceiveSideScaling"))
                sr_iov->query_rss = r;
        else if (streq(lvalue, "Trust"))
                sr_iov->trust = r;
        else
                assert_not_reached("Invalid lvalue");

        TAKE_PTR(sr_iov);
        return 0;
}

int config_parse_sr_iov_mac(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        _cleanup_(sr_iov_free_or_set_invalidp) SRIOV *sr_iov = NULL;
        Network *network = data;
        int r;

        assert(filename);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        r = sr_iov_new_static(network, filename, section_line, &sr_iov);
        if (r < 0)
                return r;

        if (isempty(rvalue)) {
                sr_iov->mac = ETHER_ADDR_NULL;
                TAKE_PTR(sr_iov);
                return 0;
        }

        r = ether_addr_from_string(rvalue, &sr_iov->mac);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse SR-IOV '%s=', ignoring assignment: %s", lvalue, rvalue);
                return 0;
        }

        TAKE_PTR(sr_iov);
        return 0;
}
