On 26 Jul 2026, at 17:33, Eli Britstein wrote:
> Introduce a new netdev type - "doca".
> The code is placed in new files.
> - ovs-doca: initialization of doca library and utility functions that
> are used currently by netdev-doca and also will be used for future
> hw-offload code.
> - netdev-doca: implementation of the new netdev.
>
> Supported ports are mlx5 ports in switch-dev mode only that with a NIC
> that supports hw-steering.
>
> The netdev has the concept of ESW manager. A representor port is
> functional only if its ESW manager is attached to OVS. In case it is
> not, the representor appears as functional in ovs-vsctl show, but it is
> not. Upon initializing of an ESW manager port, each representor is
> reconfigured to be functional, and upon destruction, they are first stopped.
>
> Steering infrastructure:
> - RX packets of all ports are steered to a common queue. This queue is
> polled using dpdk API and the packets are classified to a per-port
> memory structure.
> - TX packets are marked with the target port as metadata and sent to a
> common queue. The egress pipe matches on the metadata and forwards the
> packets accordingly.
>
> Signed-off-by: Eli Britstein <[email protected]>
Hi Eli,
Thanks for sending the v6 series. I have some comments below.
I would suggest waiting for feedback from David before sending a v7.
I have acked all of the previous patches in the series, but he may
have additional comments.
Cheers,
Eelco
[...]
> +static int
> +netdev_doca_slowpath_esw_init(struct netdev *netdev)
> +{
> + int rv;
> +
> +#define ESW_INIT_CMD(func) \
> + do { \
> + rv = (func)(netdev); \
> + if (rv == DOCA_SUCCESS) { \
> + break; \
> + } \
> + VLOG_ERR("%s: eSwitch initialization failed, %s() with error: " \
> + "%d (%s)", netdev_get_name(netdev), #func, rv, \
> + doca_error_get_descr(rv)); \
> + return rv; \
> + } while (0)
> +
> + ESW_INIT_CMD(netdev_doca_egress_pipe_init);
> + ESW_INIT_CMD(netdev_doca_rss_pipe_init);
> + ESW_INIT_CMD(netdev_doca_meta_tag0_pipe_init);
> + ESW_INIT_CMD(netdev_doca_meta_tag0_rule_init);
> + ESW_INIT_CMD(netdev_doca_pre_miss_pipe_init);
> + ESW_INIT_CMD(netdev_doca_pre_miss_rules_init);
> + ESW_INIT_CMD(netdev_doca_root_pipe_init);
If a failure occurs in a later call, resources initialized by earlier
successful calls may not be cleaned up. Should the macro call
netdev_doca_slowpath_esw_uninit() before returning on error?
netdev_doca_slowpath_esw_uninit() calls ovs_doca_destroy_pipe() for
each resource, which safely handles NULL pointers.
> + return 0;
> +}
[...]
> +static void
> +netdev_doca_dev_close(struct netdev_doca *dev)
> +{
> + struct netdev_dpdk_common *common = &dev->common;
> + struct netdev_doca_esw_ctx *esw = dev->esw_ctx;
> + struct rte_eth_dev_info dev_info;
> + char *pci_addr;
> + bool last;
> + int err;
> +
> + memset(&dev_info, 0, sizeof dev_info);
> +
> + if (rte_eth_dev_is_valid_port(common->port_id)) {
> + err = rte_eth_dev_info_get(common->port_id, &dev_info);
> + if (err) {
> + VLOG_ERR("Failed to get info of port "DPDK_PORT_ID_FMT": %s",
> + common->port_id, rte_strerror(-err));
> + }
> +
> + err = rte_eth_dev_close(common->port_id);
> + if (err) {
> + VLOG_ERR("Failed to close port "DPDK_PORT_ID_FMT": %s",
> + common->port_id, rte_strerror(-err));
> + }
> + }
> +
> + if (!esw) {
> + return;
> + }
> +
> + pci_addr = xstrdup(esw->pci_addr);
> +
> + last = refmap_unref(netdev_doca_esw_rfm, esw);
> + /* The last is the ESW. */
> + if (last && esw->dev) {
> + if (dev_info.device) {
> + /* The esw->cmd_fd is closed inside. */
> + err = rte_dev_remove(dev_info.device);
> + if (err) {
> + VLOG_ERR("Failed to remove device %s: %s", common->devargs,
> + rte_strerror(-err));
> + }
> +
> + esw->cmd_fd = -1;
> + }
> +
> + VLOG_DBG("Closing '%s'", pci_addr);
> + err = doca_dev_close(esw->dev);
> + if (err != DOCA_SUCCESS) {
> + VLOG_ERR("Failed to close doca dev %s. Error: %d (%s)", pci_addr,
> + err, doca_error_get_descr(err));
> + }
> +
> + esw->dev = NULL;
> + if (esw->cmd_fd != -1) {
> + close(esw->cmd_fd);
I guess this needs;
esw->cmd_fd = -1;
> + } else {
We do not need this else branch.
> + esw->cmd_fd = -1;
> + }
> + }
> +
> + dev->esw_ctx = NULL;
> + free(pci_addr);
> +}
[...]
> +static int
> +netdev_doca_reconfigure(struct netdev *netdev)
> +{
> + struct netdev_doca *dev = netdev_doca_cast(netdev);
> + struct netdev_dpdk_common *common = &dev->common;
> + int err = 0;
> +
> + /* If an ESW manager is not attached to OVS, a representor cannot be
> + * configured. */
> + if (!netdev_doca_is_esw_mgr(netdev) &&
> + netdev_doca_get_esw_mgr_port_id(netdev) ==
> + DPDK_ETH_PORT_ID_INVALID) {
> + return EOPNOTSUPP;
> + }
> +
> + ovs_mutex_lock(&dpdk_common_mutex);
> + ovs_mutex_lock(&dev->common.mutex);
> +
> + common->requested_n_rxq = common->user_n_rxq;
> +
> + if (netdev->n_txq == common->requested_n_txq
> + && netdev->n_rxq == common->requested_n_rxq
> + && common->mtu == common->requested_mtu
> + && common->lsc_interrupt_mode == common->requested_lsc_interrupt_mode
> + && common->rxq_size == common->requested_rxq_size
> + && common->txq_size == common->requested_txq_size
> + && eth_addr_equals(common->hwaddr, common->requested_hwaddr)
> + && common->socket_id == common->requested_socket_id
> + && netdev_dpdk_is_started(common)) {
> + /* Reconfiguration is unnecessary. */
> + goto out;
> + }
> +
> + netdev_doca_port_stop(netdev);
> +
> + err = netdev_doca_mempool_configure(dev);
> + if (err) {
> + goto out;
> + }
> +
> + common->lsc_interrupt_mode = common->requested_lsc_interrupt_mode;
> +
> + netdev->n_txq = common->requested_n_txq;
> + netdev->n_rxq = common->requested_n_rxq;
> + if (!netdev_doca_is_esw_mgr(netdev)) {
> + int esw_n_rxq;
> +
> + esw_n_rxq = dev->esw_ctx->n_rxq;
> + if (common->requested_n_rxq != esw_n_rxq) {
> + VLOG_WARN("%s: requested_n_rxq=%d is ignored. DOCA binds the "
> + "number of rx queues to the esw's n_rxq=%d",
> + netdev_get_name(netdev), common->requested_n_rxq,
> + esw_n_rxq);
> + }
> +
> + netdev->n_rxq = esw_n_rxq;
> + }
> +
> + common->rxq_size = common->requested_rxq_size;
> + common->txq_size = common->requested_txq_size;
> +
> + rte_free(common->tx_q);
> + common->tx_q = NULL;
> +
> + if (!eth_addr_equals(common->hwaddr, common->requested_hwaddr)) {
> + err = netdev_dpdk_set_dev_etheraddr(&dev->common,
> + common->requested_hwaddr);
> + if (err) {
> + goto out;
> + }
> + }
> +
> + err = doca_eth_dev_init(dev);
> + if (err) {
> + goto out;
> + }
> +
> + netdev_dpdk_update_netdev_flags(&dev->common);
> +
> + /* If both requested and actual hw-addr were previously
> + * unset (initialized to 0), then first device init above
> + * will have set actual hw-addr to something new.
> + * This would trigger spurious MAC reconfiguration unless
> + * the requested MAC is kept in sync.
> + *
> + * This is harmless in case requested_hwaddr was
> + * configured by the user, as netdev_dpdk_set_dev_etheraddr()
> + * will have succeeded to get to this point. */
> + common->requested_hwaddr = common->hwaddr;
> +
> + common->tx_q = netdev_dpdk_alloc_txq(netdev->n_txq);
> + if (!common->tx_q) {
> + err = ENOMEM;
If the allocation fails, the port is left started with a NULL tx_q.
On the next reconfigure call, the "no change needed" guard sees
started=true and skips the allocation, leaving tx_q permanently NULL
and risking a crash in netdev_doca_send() when concurrent_txq is true.
> + }
> +
> + netdev_change_seq_changed(netdev);
> +
> +out:
> + ovs_mutex_unlock(&dev->common.mutex);
> + ovs_mutex_unlock(&dpdk_common_mutex);
> + return err;
> +}
[...]
> +static enum doca_log_level
> +get_buf_log_level(const char **pbuf, size_t *psize)
> +{
> + const char *buf = *pbuf;
> + size_t size = *psize;
> + const char *p = buf;
> + int level;
> +
> + /* Skip [timestamp][thread_id][DOCA], then parse [LEVEL] (INF/WRN/etc.).
> */
> + for (int i = 0; i < 4; i++) {
> + while (size && *p && *p != '[') {
> + size--;
> + p++;
> + }
> +
> + if (!size || !*p) {
> + return DOCA_LOG_LEVEL_DISABLE;
> + }
> +
> + size--;
> + p++;
> + }
> +
Does this code need a "size >= 4" guard to safeguard the *psize
arithmetic below against underflow? Placing it here also ensures
that at least 3 bytes remain for the strncmp in
ovs_doca_parse_log_level().
if (size < 4) {
return DOCA_LOG_LEVEL_DISABLE;
}
> + level = ovs_doca_parse_log_level(NULL, p);
> + if (level < 0) {
> + return DOCA_LOG_LEVEL_DISABLE;
> + }
> +
> + /* 'p' points to the level start which is 3 chars and another ']'
> + * after it. For example "INF]". Skip it. */
> + *pbuf = p + 4;
> + *psize -= *pbuf - buf;
> +
> + return level;
> +}
> +
> +static ssize_t
> +ovs_doca_log_write(void *c OVS_UNUSED, const char *buf, size_t size)
> +{
> + static struct vlog_rate_limit dbg_rl = VLOG_RATE_LIMIT_INIT(600, 600);
> + enum doca_log_level level = get_buf_log_level(&buf, &size);
> +
> + switch (level) {
> + case DOCA_LOG_LEVEL_DISABLE:
> + VLOG_ERR("(Failed to parse level): %.*s", (int) size, buf);
> + break;
The coding style guide says 'case' labels should align with the
'switch' keyword, not be indented an extra level inside it.
> + case DOCA_LOG_LEVEL_TRACE:
> + case DOCA_LOG_LEVEL_DEBUG:
> + VLOG_DBG_RL(&dbg_rl, "%.*s", (int) size, buf);
> + break;
> + case DOCA_LOG_LEVEL_INFO:
> + VLOG_INFO_RL(&dbg_rl, "%.*s", (int) size, buf);
> + break;
> + case DOCA_LOG_LEVEL_WARNING:
> + VLOG_WARN_RL(&dbg_rl, "%.*s", (int) size, buf);
> + break;
> + case DOCA_LOG_LEVEL_ERROR:
> + VLOG_ERR_RL(&dbg_rl, "%.*s", (int) size, buf);
> + break;
> + case DOCA_LOG_LEVEL_CRIT:
> + VLOG_EMER("%.*s", (int) size, buf);
> + break;
> + }
> +
> + return size;
> +}
[...]
> +/* Every doca-entry operation is asynchronous. It must be processed using
> + * doca_flow_entries_process() API. For each processed entry, this callback
> + * is called. The 'qid' argument is the queue-id for which the entry was
> + * processed on (which is the same as the one of the initial operation).
> + * 'queues' is an array of queues. Each entry is accessed only the its own
> + * queue (which is assigned to a thread), no locks are required here. */
The last sentence here needs some rewording.
> +static void
> +ovs_doca_entry_process(struct doca_flow_pipe_entry *entry,
> + uint16_t qid,
> + enum doca_flow_entry_status status,
> + enum doca_flow_entry_op op,
> + void *aux)
> +{
> + static const char *status_desc[] = {
> + [DOCA_FLOW_ENTRY_STATUS_IN_PROCESS] = "in-process",
> + [DOCA_FLOW_ENTRY_STATUS_SUCCESS] = "success",
> + [DOCA_FLOW_ENTRY_STATUS_ERROR] = "failure",
> + };
> + static const char *op_desc[] = {
> + [DOCA_FLOW_ENTRY_OP_ADD] = "add",
> + [DOCA_FLOW_ENTRY_OP_DEL] = "del",
> + [DOCA_FLOW_ENTRY_OP_UPD] = "mod",
> + [DOCA_FLOW_ENTRY_OP_AGED] = "age",
> + };
> + bool error = status == DOCA_FLOW_ENTRY_STATUS_ERROR;
> + struct ovs_doca_steering_queue *queues = aux;
> +
> + ovs_assert(status < ARRAY_SIZE(status_desc));
> + ovs_assert(op < ARRAY_SIZE(op_desc));
> +
> + VLOG_RL(&rl, error ? VLL_ERR : VLL_DBG,
> + "%s: [qid:%" PRIu16 "] %s aux=%p entry %p %s",
> + __func__, qid, op_desc[op], aux, entry, status_desc[status]);
> +
> + if (queues && status != DOCA_FLOW_ENTRY_STATUS_IN_PROCESS) {
> + queues[qid].n_waiting_entries--;
> + }
> +}
[...]
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev