#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0

"""
Tests for RSS hashing on IPv6 Flow Label.
"""

import glob
import os
import socket
from lib.py import CmdExitFailure
from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge, ksft_in, \
    ksft_not_in, ksft_raises, KsftSkipEx
from lib.py import bkg, cmd, defer, fd_read_timeout, rand_port
from lib.py import NetDrvEpEnv


def _check_system(cfg):
    if not hasattr(socket, "SO_INCOMING_CPU"):
        raise KsftSkipEx("socket.SO_INCOMING_CPU was added in Python 3.11")

    qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
    if qcnt < 2:
        raise KsftSkipEx(f"Local has only {qcnt} queues")

    for f in [f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_flow_cnt",
              f"/sys/class/net/{cfg.ifname}/queues/rx-0/rps_cpus"]:
        try:
            with open(f, 'r') as fp:
                setting = fp.read().strip()
                # CPU mask will be zeros and commas
                if setting.replace("0", "").replace(",", ""):
                    raise KsftSkipEx(f"RPS/RFS is configured: {f}: {setting}")
        except FileNotFoundError:
            pass

    # 1 is the default, if someone changed it we probably shouldn"t mess with it
    af = cmd("cat /proc/sys/net/ipv6/auto_flowlabels", host=cfg.remote).stdout
    if af.strip() != "1":
        raise KsftSkipEx("Remote does not have auto_flowlabels enabled")


def _ethtool_get_cfg(cfg, fl_type):
    descr = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout

    converter = {
        "IP SA": "s",
        "IP DA": "d",
        "L3 proto": "t",
        "L4 bytes 0 & 1 [TCP/UDP src port]": "f",
        "L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
        "IPv6 Flow Label": "l",
    }

    ret = ""
    for line in descr.split("\n")[1:-2]:
        # if this raises we probably need to add more keys to converter above
        ret += converter[line]
    return ret


def _traffic(cfg, one_sock, one_cpu):
    local_port  = rand_port(socket.SOCK_DGRAM)
    remote_port = rand_port(socket.SOCK_DGRAM)

    sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
    sock.bind(("", local_port))
    sock.connect((cfg.remote_addr_v["6"], 0))
    if one_sock:
        send = f"exec 5<>/dev/udp/{cfg.addr_v['6']}/{local_port}; " \
                "for i in `seq 20`; do echo a >&5; sleep 0.02; done; exec 5>&-"
    else:
        send = "for i in `seq 20`; do echo a | socat -t0.02 - UDP6:" \
              f"[{cfg.addr_v['6']}]:{local_port},sourceport={remote_port}; done"

    cpus = set()
    with bkg(send, shell=True, host=cfg.remote, exit_wait=True):
        for _ in range(20):
            fd_read_timeout(sock.fileno(), 1)
            cpu = sock.getsockopt(socket.SOL_SOCKET, socket.SO_INCOMING_CPU)
            cpus.add(cpu)

    if one_cpu:
        ksft_eq(len(cpus), 1,
                f"{one_sock=} - expected one CPU, got traffic on: {cpus=}")
    else:
        ksft_ge(len(cpus), 2,
                f"{one_sock=} - expected many CPUs, got traffic on: {cpus=}")


def test_rss_flow_label(cfg):
    """
    Test hashing on IPv6 flow label. Send traffic over a single socket
    and over multiple sockets. Depend on the remote having auto-label
    enabled so that it randomizes the label per socket.
    """

    cfg.require_ipver("6")
    cfg.require_cmd("socat", remote=True)
    _check_system(cfg)

    # Enable flow label hashing for UDP6
    initial = _ethtool_get_cfg(cfg, "udp6")
    no_lbl = initial.replace("l", "")
    if "l" not in initial:
        try:
            cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
        except CmdExitFailure as exc:
            raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc

        defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")

    _traffic(cfg, one_sock=True, one_cpu=True)
    _traffic(cfg, one_sock=False, one_cpu=False)

    # Disable it, we should see no hashing (reset was already defer()ed)
    cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")

    _traffic(cfg, one_sock=False, one_cpu=True)


def _check_v4_flow_types(cfg):
    for fl_type in ["tcp4", "udp4", "ah4", "esp4", "sctp4"]:
        try:
            cur = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}").stdout
            ksft_not_in("Flow Label", cur,
                        comment=f"{fl_type=} has Flow Label:" + cur)
        except CmdExitFailure:
            # Probably does not support this flow type
            pass


def test_rss_flow_label_6only(cfg):
    """
    Test interactions with IPv4 flow types. It should not be possible to set
    IPv6 Flow Label hashing for an IPv4 flow type. The Flow Label should also
    not appear in the IPv4 "current config".
    """

    with ksft_raises(CmdExitFailure) as cm:
        cmd(f"ethtool -N {cfg.ifname} rx-flow-hash tcp4 sdfnl")
    ksft_in("Invalid argument", cm.exception.cmd.stderr)

    _check_v4_flow_types(cfg)

    # Try to enable Flow Labels and check again, in case it leaks thru
    initial = _ethtool_get_cfg(cfg, "udp6")
    no_lbl = initial.replace("l", "")
    if "l" not in initial:
        try:
            cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 l{no_lbl}")
        except CmdExitFailure as exc:
            raise KsftSkipEx("Device doesn't support Flow Label for UDP6") from exc
    else:
        cmd(f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {no_lbl}")
    restore = defer(cmd, f"ethtool -N {cfg.ifname} rx-flow-hash udp6 {initial}")

    _check_v4_flow_types(cfg)
    restore.exec()
    _check_v4_flow_types(cfg)


def main() -> None:
    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
        ksft_run([test_rss_flow_label,
                  test_rss_flow_label_6only],
                 args=(cfg, ))
    ksft_exit()


if __name__ == "__main__":
    main()
