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

"""
Devlink Rate Cross-eswitch Scheduling Test Suite
==================================================

Control-plane tests for cross-eswitch TX scheduling via devlink-rate.
Validates that VFs from different PFs on the same chip can share
rate groups using the cross-device parent-dev attribute.

Preconditions:
- NETIF points to a bond device with exactly two interfaces.
- the interfaces must be two PFs from different devices sharing the same chip.
- (for mlx5): the two interfaces are in switchdev mode and configured in a LAG:
  - devlink dev eswitch set $DEV1 mode switchdev
  - devlink dev eswitch set $DEV2 mode switchdev
  - devlink dev param set $DEV1 name esw_multiport value 1 cmode runtime
  - devlink dev param set $DEV2 name esw_multiport value 1 cmode runtime
- test cases will be skipped if:
  - the number of interfaces in the bond device is != 2.
  - the kernel doesn't support devlink rates.
  - the devlink API doesn't support cross-device parents (ENODEV).
  - cross-esw rate scheduling returns EOPNOTSUPP.
"""

import errno
import glob
import os
import time

from lib.py import ksft_pr, ksft_eq, ksft_run, ksft_exit
from lib.py import KsftSkipEx, KsftFailEx
from lib.py import NetDrvEnv, DevlinkFamily
from lib.py import NlError
from lib.py import cmd, defer, ip, tool


# --- Discovery and setup ---


def get_bond_slaves(bond_ifname):
    """Returns sorted list of slave netdev names for a bond."""
    pattern = f"/sys/class/net/{bond_ifname}/lower_*"
    lowers = glob.glob(pattern)
    if not lowers:
        raise KsftSkipEx(f"No bond slaves for {bond_ifname}")
    slaves = []
    for path in sorted(lowers):
        name = os.path.basename(path)
        if name.startswith("lower_"):
            name = name[len("lower_"):]
        slaves.append(name)
    return slaves


def discover_pfs(cfg):
    """Discovers both PFs from bond slaves."""
    slaves = get_bond_slaves(cfg.ifname)
    if len(slaves) != 2:
        raise KsftSkipEx(f"Need 2 bond slaves, found {len(slaves)}")

    pf0, pf1 = slaves[0], slaves[1]
    ksft_pr(f"PF0: {pf0} PF1: {pf1}")
    return pf0, pf1


def get_pci_addr(ifname):
    """Resolves PCI address for a network interface."""
    return os.path.basename(os.path.realpath(f"/sys/class/net/{ifname}/device"))


def get_vf_port_index(pf_pci):
    """Finds devlink port-index for vf0 under pf_pci."""
    ports = tool("devlink", "port show", json=True)["port"]
    for port_name, props in ports.items():
        if port_name.startswith(f"pci/{pf_pci}/") and props.get("vfnum") == 0:
            return int(port_name.split("/")[-1])
    raise KsftSkipEx(f"VF port not found for {pf_pci}")


def cleanup_esw(pf):
    """Removes VFs if created by tests."""
    cmd(f"echo 0 > /sys/class/net/{pf}/device/sriov_numvfs", shell=True, fail=False)


def setup_esw(pf):
    """Creates 1 VF on 'pf'."""
    path = f"/sys/class/net/{pf}/device/sriov_numvfs"
    cmd(f"echo 0 > {path}", shell=True)
    cmd(f"echo 1 > {path}", shell=True)
    defer(cleanup_esw, pf)
    time.sleep(2)

    vf_dir = f"/sys/class/net/{pf}/device/virtfn0/net"
    entries = os.listdir(vf_dir) if os.path.isdir(vf_dir) else []
    if not entries:
        raise KsftSkipEx(f"VF not found for {pf}")
    ip(f"link set dev {entries[0]} up")

    pf_pci = get_pci_addr(pf)
    vf_idx = get_vf_port_index(pf_pci)
    ksft_pr(f"Created VF {vf_idx} on PF {pf} ({pf_pci})")
    return pf_pci, vf_idx


# --- Rate operation helpers ---


def rate_new(devnl, dev_pci, node_name, **kwargs):
    """Creates rate node."""
    params = {
        "bus-name": "pci",
        "dev-name": dev_pci,
        "rate-node-name": node_name,
    }
    params.update(kwargs)
    try:
        devnl.rate_new(params)
    except NlError as e:
        if e.error == errno.EOPNOTSUPP:
            raise KsftSkipEx("rate_new not supported") from e
        raise KsftFailEx("rate_new failed") from e


def rate_get(devnl, dev_pci, node_name):
    """Gets rate node."""
    params = {
        "bus-name": "pci",
        "dev-name": dev_pci,
        "rate-node-name": node_name,
    }
    return devnl.rate_get(params)


def rate_get_leaf(devnl, dev_pci, port_index):
    """Gets rate leaf (VF)."""
    params = {
        "bus-name": "pci",
        "dev-name": dev_pci,
        "port-index": port_index,
    }
    return devnl.rate_get(params)


def rate_del(devnl, dev_pci, node_name):
    """Deletes rate node."""
    devnl.rate_del({
        "bus-name": "pci",
        "dev-name": dev_pci,
        "rate-node-name": node_name,
    })


def rate_set_leaf(devnl, dev_pci, port_index, **kwargs):
    """Sets rate attributes on a leaf (VF)."""
    params = {
        "bus-name": "pci",
        "dev-name": dev_pci,
        "port-index": port_index,
    }
    params.update(kwargs)
    try:
        devnl.rate_set(params)
    except NlError as e:
        if e.error == errno.EOPNOTSUPP:
            raise KsftSkipEx("rate_set not supported") from e
        raise KsftFailEx("rate_set failed") from e


def rate_set_leaf_parent(devnl, dev_pci, port_index,
                         parent_name, parent_dev_pci=None):
    """Sets a leaf's parent, optionally cross-esw."""
    params = {
        "bus-name": "pci",
        "dev-name": dev_pci,
        "port-index": port_index,
        "rate-parent-node-name": parent_name,
    }
    if parent_dev_pci:
        params["parent-dev"] = {
            "bus-name": "pci",
            "dev-name": parent_dev_pci,
        }
    try:
        devnl.rate_set(params)
    except NlError as e:
        if e.error == errno.EOPNOTSUPP:
            raise KsftSkipEx("rate_set not supported") from e
        if parent_dev_pci and e.error == errno.ENODEV:
            raise KsftSkipEx("Cross-esw scheduling not supported") from e
        raise KsftFailEx("rate_set failed") from e


def rate_clear_leaf_parent(devnl, dev_pci, port_index):
    """Clears a leaf's parent."""
    rate_set_leaf_parent(devnl, dev_pci, port_index, "")


def rate_set_node(devnl, dev_pci, node_name, **kwargs):
    """Sets rate attributes on a node."""
    params = {
        "bus-name": "pci",
        "dev-name": dev_pci,
        "rate-node-name": node_name,
    }
    params.update(kwargs)
    devnl.rate_set(params)


# --- Test cases ---


def test_same_esw_parent(cfg):
    """Assigns PF0's VF to PF0's group (same esw baseline)."""
    pf0, _ = discover_pfs(cfg)
    pf0_pci, vf0_idx = setup_esw(pf0)

    rate_new(cfg.devnl, pf0_pci, "group0")
    defer(rate_del, cfg.devnl, pf0_pci, "group0")
    ksft_pr("rate-new succeeded")

    rate_set_leaf_parent(cfg.devnl, pf0_pci, vf0_idx, "group0")
    defer(rate_clear_leaf_parent, cfg.devnl, pf0_pci, vf0_idx)

    ksft_pr("Same-esw parent assignment succeeded")


def test_cross_esw_parent(cfg):
    """Sets cross-esw parent, then clear it."""
    pf0, pf1 = discover_pfs(cfg)
    pf0_pci, _ = setup_esw(pf0)
    pf1_pci, vf1_idx = setup_esw(pf1)

    rate_new(cfg.devnl, pf0_pci, "group1")
    defer(rate_del, cfg.devnl, pf0_pci, "group1")
    ksft_pr("rate-new succeeded")

    rate_set_leaf_parent(cfg.devnl, pf1_pci, vf1_idx,
                         "group1", parent_dev_pci=pf0_pci)
    defer(rate_clear_leaf_parent, cfg.devnl, pf1_pci, vf1_idx)

    ksft_pr("Cross-esw parent set and clear succeeded")


def test_tx_rates_on_cross_esw(cfg):
    """Sets tx_max on group and tx_share on leaves in a cross-esw setup."""
    pf0, pf1 = discover_pfs(cfg)
    pf0_pci, vf0_idx = setup_esw(pf0)
    pf1_pci, vf1_idx = setup_esw(pf1)

    rate_new(cfg.devnl, pf0_pci, "group2", **{"rate-tx-max": 10000000})
    defer(rate_del, cfg.devnl, pf0_pci, "group2")
    ksft_pr("rate-new succeeded")

    rate_set_leaf_parent(cfg.devnl, pf1_pci, vf1_idx,
                         "group2", parent_dev_pci=pf0_pci)
    defer(rate_clear_leaf_parent, cfg.devnl, pf1_pci, vf1_idx)
    ksft_pr("set parent cross-esw succeeded")

    rate_set_leaf_parent(cfg.devnl, pf0_pci, vf0_idx, "group2")
    defer(rate_clear_leaf_parent, cfg.devnl, pf0_pci, vf0_idx)
    ksft_pr("set parent same esw succeeded")

    rate_set_leaf(cfg.devnl, pf0_pci, vf0_idx, **{"rate-tx-share": 1000000})
    rate = rate_get_leaf(cfg.devnl, pf0_pci, vf0_idx)
    ksft_eq(rate["rate-tx-share"], 1000000)
    rate_set_leaf(cfg.devnl, pf1_pci, vf1_idx, **{"rate-tx-share": 2000000})
    rate = rate_get_leaf(cfg.devnl, pf1_pci, vf1_idx)
    ksft_eq(rate["rate-tx-share"], 2000000)
    rate_set_node(cfg.devnl, pf0_pci, "group2", **{"rate-tx-max": 250000000})
    rate = rate_get(cfg.devnl, pf0_pci, "group2")
    ksft_eq(rate["rate-tx-max"], 250000000)

    ksft_pr("tx_max and tx_share set on cross-esw group")


def main() -> None:
    """Main function."""

    with NetDrvEnv(__file__, nsim_test=False) as cfg:
        cfg.devnl = DevlinkFamily()

        ksft_run(
            cases=[
                test_same_esw_parent,
                test_cross_esw_parent,
                test_tx_rates_on_cross_esw,
            ],
            args=(cfg,),
        )
    ksft_exit()


if __name__ == "__main__":
    main()