[ovs-dev] [RFC PATCH] lib: Introduce netlink-devlink library

Frode Nordahl frode.nordahl at canonical.com
Tue Mar 23 14:50:32 UTC 2021


The devlink interface was introduced [0] in the Linux 4.6 time
frame and has since gained traction among multiple hardware
vendors.

The devlink-port [1] and devlink-info[1] interfaces are
particularly useful for managing NICs connected to multiple
distinct CPUs such as SmartNICs.

In such a topology it would be useful to be able to offload
Open vSwitch and OVN onto the NIC SoC operating system and this
library will help with discovering and managing ports representing
resources made available on the host from the NIC SoC side.

The library will be consumed by upcoming proposed changes to OVN,
and I think it makes sense to maintain it together with the Open
vSwitch netlink library code.

0: https://lore.kernel.org/netdev/1456504351-18871-1-git-send-email-jiri@resnulli.us/
1: https://github.com/torvalds/linux/blob/master/Documentation/networking/devlink/devlink-port.rst
2: https://github.com/torvalds/linux/blob/master/Documentation/networking/devlink/devlink-info.rst

TODO:
- Polish a small (non-install) utility that can be used for testing
  dump and monitoring of devlink changes.
- Write tests

Signed-off-by: Frode Nordahl <frode.nordahl at canonical.com>
---
 acinclude.m4                |  80 ++++++++
 configure.ac                |   1 +
 include/linux/automake.mk   |   1 +
 include/linux/devlink.h     | 122 +++++++++++
 include/openvswitch/types.h |   8 +
 lib/automake.mk             |   6 +
 lib/netlink-devlink.c       | 394 ++++++++++++++++++++++++++++++++++++
 lib/netlink-devlink.h       | 144 +++++++++++++
 lib/netlink.c               |  16 ++
 lib/netlink.h               |   5 +
 lib/packets.h               |   1 +
 utilities/.gitignore        |   1 +
 utilities/automake.mk       |   5 +
 utilities/devlink.c         |  67 ++++++
 14 files changed, 851 insertions(+)
 create mode 100644 include/linux/devlink.h
 create mode 100644 lib/netlink-devlink.c
 create mode 100644 lib/netlink-devlink.h
 create mode 100644 utilities/devlink.c

diff --git a/acinclude.m4 b/acinclude.m4
index 15a54d636..3ac4ff6aa 100644
--- a/acinclude.m4
+++ b/acinclude.m4
@@ -203,6 +203,86 @@ AC_DEFUN([OVS_CHECK_LINUX_NETLINK], [
     [Define to 1 if struct nla_bitfield32 is available.])])
 ])
 
+dnl OVS_CHECK_LINUX_DEVLINK
+dnl
+dnl Check whether we should build devlink support and configure Linux devlink
+dnl compat. Minimum required kernel version v4.6.
+AC_DEFUN([OVS_CHECK_LINUX_DEVLINK], [
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = 1;
+    ])],
+    [ovs_cv_devlink=true], [ovs_cv_devlink=false])
+  if test X"$ovs_cv_devlink" = Xtrue; then
+    AC_DEFINE([HAVE_DEVLINK], [1],
+              [Define to 1 if kernel has DEVLINK support available.])
+  fi
+  AM_CONDITIONAL([DEVLINK], [$ovs_cv_devlink])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_BUS_NAME;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_BUS_NAME], [1],
+               [Define to 1 if DEVLINK_ATTR_BUS_NAME is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_PORT_FLAVOUR;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_PORT_FLAVOUR], [1],
+               [Define to 1 if DEVLINK_ATTR_PORT_FLAVOUR is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_INFO_DRIVER_NAME;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_INFO_DRIVER_NAME], [1],
+               [Define to 1 if DEVLINK_ATTR_INFO_DRIVER_NAME is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_PORT_PCI_PF_NUMBER;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_PORT_PCI_PF_NUMBER], [1],
+               [Define to 1 if DEVLINK_ATTR_PORT_PCI_PF_NUMBER is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_PORT_FUNCTION;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_PORT_FUNCTION], [1],
+               [Define to 1 if DEVLINK_ATTR_PORT_FUNCTION is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_PORT_EXTERNAL;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_PORT_EXTERNAL], [1],
+               [Define to 1 if DEVLINK_ATTR_PORT_EXTERNAL is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_PORT_PCI_SF_NUMBER;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_PORT_PCI_SF_NUMBER], [1],
+               [Define to 1 if DEVLINK_ATTR_PORT_PCI_SF_NUMBER is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_INFO_DRIVER_NAME;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_INFO_DRIVER_NAME], [1],
+               [Define to 1 if DEVLINK_ATTR_INFO_DRIVER_NAME is available.])])
+
+  AC_COMPILE_IFELSE([
+    AC_LANG_PROGRAM([#include <linux/devlink.h>], [
+        int x = DEVLINK_ATTR_INFO_BOARD_SERIAL_NUMBER;
+    ])],
+    [AC_DEFINE([HAVE_DEVLINK_ATTR_INFO_BOARD_SERIAL_NUMBER], [1],
+               [Define to 1 if DEVLINK_ATTR_INFO_BOARD_SERIAL_NUMBER is available.])])
+])
+
 dnl OVS_CHECK_LINUX_TC
 dnl
 dnl Configure Linux tc compat.
diff --git a/configure.ac b/configure.ac
index c077034d4..82342e18f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -191,6 +191,7 @@ AC_ARG_VAR(KARCH, [Kernel Architecture String])
 AC_SUBST(KARCH)
 OVS_CHECK_LINUX
 OVS_CHECK_LINUX_NETLINK
+OVS_CHECK_LINUX_DEVLINK
 OVS_CHECK_LINUX_TC
 OVS_CHECK_LINUX_SCTP_CT
 OVS_CHECK_LINUX_VIRTIO_TYPES
diff --git a/include/linux/automake.mk b/include/linux/automake.mk
index 8f063f482..8718f980d 100644
--- a/include/linux/automake.mk
+++ b/include/linux/automake.mk
@@ -1,4 +1,5 @@
 noinst_HEADERS += \
+	include/linux/devlink.h \
 	include/linux/netlink.h \
 	include/linux/netfilter/nf_conntrack_sctp.h \
 	include/linux/pkt_cls.h \
diff --git a/include/linux/devlink.h b/include/linux/devlink.h
new file mode 100644
index 000000000..4f655a6ce
--- /dev/null
+++ b/include/linux/devlink.h
@@ -0,0 +1,122 @@
+#ifndef __UAPI_LINUX_DEVLINK_WRAPPER_H
+#define __UAPI_LINUX_DEVLINK_WRAPPER_H 1
+
+/*
+ * devlink_command
+ */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_BUS_NAME)
+
+/* Appeared in Linux v4.6 */
+#define DEVLINK_CMD_PORT_GET                   0x05
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_BUS_NAME */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_INFO_DRIVER_NAME)
+
+/* Appeared in Linux v5.1 */
+#define DEVLINK_CMD_INFO_GET                   0x51
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_INFO_DRIVER_NAME */
+
+/*
+ * devlink_attr
+ */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_BUS_NAME)
+
+/* Appeared in Linux v4.6 */
+#define DEVLINK_ATTR_BUS_NAME 0x01
+#define DEVLINK_ATTR_DEV_NAME                  0x02
+#define DEVLINK_ATTR_PORT_INDEX                0x03
+#define DEVLINK_ATTR_PORT_TYPE                 0x04
+#define DEVLINK_ATTR_PORT_DESIRED_TYPE         0x05
+#define DEVLINK_ATTR_PORT_NETDEV_IFINDEX       0x06
+#define DEVLINK_ATTR_PORT_NETDEV_NAME          0x07
+#define DEVLINK_ATTR_PORT_IBDEV_NAME           0x08
+#define DEVLINK_ATTR_PORT_SPLIT_COUNT          0x09
+#define DEVLINK_ATTR_PORT_SPLIT_GROUP          0x0a
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_BUS_NAME */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_FLAVOUR)
+
+/* Appeared in Linux v4.18 */
+#define DEVLINK_ATTR_PORT_FLAVOUR              0x4d
+#define DEVLINK_ATTR_PORT_NUMBER               0x4e
+#define DEVLINK_ATTR_PORT_SPLIT_SUBPORT_NUMBER 0x4f
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_FLAVOUR */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_INFO_DRIVER_NAME)
+
+/* Appeared in Linux v5.1 */
+#define DEVLINK_ATTR_INFO_DRIVER_NAME          0x62
+#define DEVLINK_ATTR_INFO_SERIAL_NUMBER        0x63
+#define DEVLINK_ATTR_INFO_VERSION_FIXED        0x64
+#define DEVLINK_ATTR_INFO_VERSION_RUNNING      0x65
+#define DEVLINK_ATTR_INFO_VERSION_STORED       0x66
+#define DEVLINK_ATTR_INFO_VERSION_NAME         0x67
+#define DEVLINK_ATTR_INFO_VERSION_VALUE        0x68
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_INFO_DRIVER_NAME */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_PCI_PF_NUMBER)
+
+/* Appeared in Linux v5.3 */
+#define DEVLINK_ATTR_PORT_PCI_PF_NUMBER        0x7f
+#define DEVLINK_ATTR_PORT_PCI_VF_NUMBER        0x80
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_PCI_PF_NUMBER */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_FUNCTION)
+
+/* Appeared in Linux v5.9 */
+#define DEVLINK_ATTR_PORT_FUNCTION             0x91
+#define DEVLINK_ATTR_INFO_BOARD_SERIAL_NUMBER  0x92
+#define DEVLINK_ATTR_PORT_LANES                0x93
+#define DEVLINK_ATTR_PORT_SPLITTABLE           0x94
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_FUNCTION */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_EXTERNAL)
+
+/* Appeared in Linux v5.10 */
+#define DEVLINK_ATTR_PORT_EXTERNAL             0x95
+#define DEVLINK_ATTR_PORT_CONTROLLER_NUMBER    0x96
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_EXTERNAL */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_PCI_SF_NUMBER)
+
+/* Appeared in Linux v5.12 */
+#define DEVLINK_ATTR_PORT_PCI_SF_NUMBER        0xa4
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_PCI_SF_NUMBER */
+
+/*
+ * devlink_port_function_attr
+ */
+
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_FUNCTION)
+
+/* Appeared in Linux v5.9 */
+#define DEVLINK_PORT_FUNCTION_ATTR_UNSPEC      0x00
+#define DEVLINK_PORT_FUNCTION_ATTR_HW_ADDR     0x01
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_FUNCTION */
+#if !defined(__KERNEL__) && !defined(HAVE_DEVLINK_ATTR_PORT_PCI_SF_NUMBER)
+
+/* Appeared in Linux v5.12 */
+#define DEVLINK_PORT_FN_ATTR_STATE             0x02
+#define DEVLINK_PORT_FN_ATTR_OPSTATE           0x03
+
+/*
+ * devlink_port_fn_state
+ */
+#define DEVLINK_PORT_FN_STATE_INACTIVE         0x00
+#define DEVLINK_PORT_FN_STATE_ACTIVE           0x01
+
+
+/*
+ * devlink_port_fn_opstate
+ */
+#define DEVLINK_PORT_FN_OPSTATE_DETACHED       0x00
+#define DEVLINK_PORT_FN_OPSTATE_ATTACHED       0x01
+
+#endif /* !__KERNEL__ && !HAVE_DEVLINK_ATTR_PORT_PCI_SF_NUMBER */
+
+#include_next <linux/devlink.h>
+
+#endif /* __UAPI_LINUX_DEVLINK_WRAPPER_H */
diff --git a/include/openvswitch/types.h b/include/openvswitch/types.h
index 45e70790e..069e479ae 100644
--- a/include/openvswitch/types.h
+++ b/include/openvswitch/types.h
@@ -186,6 +186,14 @@ struct eth_addr64 {
 #define ETH_ADDR64_C(A,B,C,D,E,F,G,H) \
     { { { 0x##A, 0x##B, 0x##C, 0x##D, 0x##E, 0x##F, 0x##G, 0x##H } } }
 
+/* Similar to struct eth_addr, for InfiniBand addresses. */
+struct ib_addr {
+    union {
+        uint8_t ea[20];
+        ovs_be16 be16[10];
+    };
+};
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/lib/automake.mk b/lib/automake.mk
index 39afbff9d..084802088 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -448,6 +448,12 @@ lib_libopenvswitch_la_SOURCES += \
 	lib/tc.h
 endif
 
+if DEVLINK
+lib_libopenvswitch_la_SOURCES += \
+	lib/netlink-devlink.c \
+	lib/netlink-devlink.h
+endif
+
 if HAVE_AF_XDP
 lib_libopenvswitch_la_SOURCES += \
 	lib/netdev-afxdp-pool.c \
diff --git a/lib/netlink-devlink.c b/lib/netlink-devlink.c
new file mode 100644
index 000000000..06a89b499
--- /dev/null
+++ b/lib/netlink-devlink.c
@@ -0,0 +1,394 @@
+/*
+ * Copyright (c) 2021 Canonical
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <config.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <linux/devlink.h>
+#include <linux/genetlink.h>
+#include "netlink.h"
+#include "netlink-socket.h"
+#include "netlink-devlink.h"
+#include "openvswitch/vlog.h"
+#include "packets.h"
+
+VLOG_DEFINE_THIS_MODULE(netlink_devlink);
+
+/* Initialized by nl_devlink_init() */
+static int ovs_devlink_family;
+
+static int nl_devlink_init(void);
+
+void
+nl_msg_put_dlgenmsg(struct ofpbuf *msg, size_t expected_payload,
+                    int family, uint8_t cmd, uint32_t flags)
+{
+    nl_msg_put_genlmsghdr(msg, expected_payload, family,
+                          flags, cmd, DEVLINK_GENL_VERSION);
+}
+
+/* Starts a Netlink-devlink "dump" operation, by sending devlink request with
+ * command 'cmd' to the kernel on a Netlink socket, and initializes 'state'
+ * with buffer and dump state.
+ *
+ * On success, 0 will be returned, the caller can call nl_dl_*_dump_next to
+ * retrieve data and must call nl_dl_dump_finish in the end to free up any
+ * allocated buffers.
+ *
+ * On failure, a non-zero value will be returned, the caller must not make any
+ * attempts to call nl_dl_*_dump_next or nl_dl_dump_finish.
+ */
+int
+nl_dl_dump_start(uint8_t cmd, struct nl_dl_dump_state *state)
+{
+    struct ofpbuf *request;
+    int error;
+
+    error = nl_devlink_init();
+    if (error) {
+        return error;
+    }
+
+    request = ofpbuf_new(NLMSG_HDRLEN + GENL_HDRLEN);
+    nl_msg_put_dlgenmsg(request, 0, ovs_devlink_family, cmd,
+                        NLM_F_REQUEST);
+    nl_dump_start(&state->dump, NETLINK_GENERIC, request);
+    ofpbuf_delete(request);
+
+    ofpbuf_init(&state->buf, NL_DUMP_BUFSIZE);
+
+    return 0;
+}
+
+static bool
+nl_dl_dump_next__(struct nl_dl_dump_state *state, void *entry)
+{
+    struct ofpbuf msg;
+
+    if (!nl_dump_next(&state->dump, &msg, &state->buf)) {
+        return false;
+    }
+    if (!nl_dl_parse_port_policy(&msg, entry)) {
+        ovs_mutex_lock(&state->dump.mutex);
+        state->dump.status = EPROTO;
+        ovs_mutex_unlock(&state->dump.mutex);
+        return false;
+    }
+    return true;
+}
+
+/* Attempts to retrieve and parse another reply in on-going dump operation.
+ *
+ * If successful, returns true and assignes values or pointers to data in
+ * 'port_entry'.  The caller must not modify 'port_entry' (because it may
+ * contain pointers to data within the buffer which will be used by future
+ * calls to this function.
+ *
+ * On failure, returns false.  Failure might indicate an actual error or merely
+ * the end of replies.  An error status for the entire dump operation is
+ * provided when it is completed by calling nl_dl_dump_finish()
+ */
+bool
+nl_dl_port_dump_next(struct nl_dl_dump_state *state,
+                     struct dl_port *port_entry)
+{
+    return nl_dl_dump_next__(state, (void*)port_entry);
+}
+
+bool
+nl_dl_info_dump_next(struct nl_dl_dump_state *state,
+                     struct dl_info *info_entry)
+{
+    return nl_dl_dump_next__(state, (void*)info_entry);
+}
+
+int
+nl_dl_dump_finish(struct nl_dl_dump_state *state)
+{
+    ofpbuf_uninit(&state->buf);
+    return nl_dump_done(&state->dump);
+}
+
+bool
+nl_dl_parse_port_function(struct nlattr *nla, struct dl_port_function *port_fn)
+{
+    static const struct nl_policy policy[] = {
+        /* Appeared in Linux v5.9 */
+        [DEVLINK_PORT_FUNCTION_ATTR_UNSPEC] = { .type = NL_A_UNSPEC,
+                                                .optional = true, },
+        [DEVLINK_PORT_FUNCTION_ATTR_HW_ADDR] = { .type = NL_A_HW_ADDR,
+                                                 .optional = true, },
+
+        /* Appeared in Linnux v5.12 */
+        [DEVLINK_PORT_FN_ATTR_STATE] = { .type = NL_A_U8, .optional = true, },
+        [DEVLINK_PORT_FN_ATTR_OPSTATE] = { .type = NL_A_U8,
+                                           .optional = true, },
+    };
+    struct nlattr *attrs[ARRAY_SIZE(policy)];
+    struct nlattr *attrp;
+    bool parsed;
+
+    parsed = nl_parse_nested(nla, policy, attrs, ARRAY_SIZE(policy));
+
+    if (parsed) {
+        attrp = attrs[DEVLINK_PORT_FUNCTION_ATTR_HW_ADDR];
+        if (attrp && nl_attr_get_size(attrp) == sizeof(struct eth_addr)) {
+            port_fn->eth_addr = nl_attr_get_eth_addr(attrp);
+        } else if (attrp && nl_attr_get_size(attrp) == sizeof(struct ib_addr)) {
+            port_fn->ib_addr = nl_attr_get_ib_addr(attrp);
+        } else {
+            memset(&port_fn->eth_addr, 0, sizeof(port_fn->eth_addr));
+            memset(&port_fn->ib_addr, 0, sizeof(port_fn->ib_addr));
+        }
+        NL_UINT_ATTR_ASSIGN(port_fn->state, DEVLINK_PORT_FN_ATTR_STATE,
+                            attrs, policy, ARRAY_SIZE(policy))
+        NL_UINT_ATTR_ASSIGN(port_fn->opstate, DEVLINK_PORT_FN_ATTR_OPSTATE,
+                            attrs, policy, ARRAY_SIZE(policy))
+    }
+
+    return parsed;
+}
+
+bool
+nl_dl_parse_port_policy(struct ofpbuf *msg, struct dl_port *port)
+{
+    static const struct nl_policy policy[] = {
+        /* Appeared in Linux v4.6 */
+        [DEVLINK_ATTR_BUS_NAME] = { .type = NL_A_STRING, .optional = false, },
+        [DEVLINK_ATTR_DEV_NAME] = { .type = NL_A_STRING, .optional = false, },
+        [DEVLINK_ATTR_PORT_INDEX] = { .type = NL_A_U32, .optional = false, },
+
+        [DEVLINK_ATTR_PORT_TYPE] = { .type = NL_A_U16, .optional = true, },
+        [DEVLINK_ATTR_PORT_DESIRED_TYPE] = { .type = NL_A_U16,
+                                            .optional = true, },
+        [DEVLINK_ATTR_PORT_NETDEV_IFINDEX] = { .type = NL_A_U32,
+                                               .optional = true, },
+        [DEVLINK_ATTR_PORT_NETDEV_NAME] = { .type = NL_A_STRING,
+                                            .optional = true, },
+        [DEVLINK_ATTR_PORT_IBDEV_NAME] = { .type = NL_A_STRING,
+                                           .optional = true, },
+        [DEVLINK_ATTR_PORT_SPLIT_COUNT] = { .type = NL_A_U32,
+                                            .optional = true, },
+        [DEVLINK_ATTR_PORT_SPLIT_GROUP] = { .type = NL_A_U32,
+                                            .optional = true, },
+
+        /* Appeared in Linux v4.18 */
+        [DEVLINK_ATTR_PORT_FLAVOUR] = { .type = NL_A_U16, .optional = true, },
+        [DEVLINK_ATTR_PORT_NUMBER] = { .type = NL_A_U32, .optional = true, },
+        [DEVLINK_ATTR_PORT_SPLIT_SUBPORT_NUMBER] = { .type = NL_A_U32,
+                                                     .optional = true, },
+
+        /* Appeared in Linux v5.3 */
+        [DEVLINK_ATTR_PORT_PCI_PF_NUMBER] = { .type = NL_A_U16,
+                                              .optional = true, },
+        [DEVLINK_ATTR_PORT_PCI_VF_NUMBER] = { .type = NL_A_U16,
+                                              .optional = true, },
+
+        /* Appeared in Linux v5.9 */
+        [DEVLINK_ATTR_PORT_FUNCTION] = { .type = NL_A_NESTED,
+                                         .optional = true, },
+        [DEVLINK_ATTR_PORT_LANES] = { .type = NL_A_U32, .optional = true, },
+        [DEVLINK_ATTR_PORT_SPLITTABLE] = { .type = NL_A_U8,
+                                           .optional = true, },
+
+        /* Appeared in Linux v5.10 */
+        [DEVLINK_ATTR_PORT_EXTERNAL] = { .type = NL_A_U8, .optional = true },
+        [DEVLINK_ATTR_PORT_CONTROLLER_NUMBER] = { .type = NL_A_U32,
+                                                  .optional = true},
+
+        /* Appeared in Linux v5.12 */
+        [DEVLINK_ATTR_PORT_PCI_SF_NUMBER] = { .type = NL_A_U32,
+                                              .optional = true },
+    };
+    struct nlattr *attrs[ARRAY_SIZE(policy)];
+
+    if (!nl_policy_parse(msg, NLMSG_HDRLEN + GENL_HDRLEN,
+                         policy, attrs,
+                         ARRAY_SIZE(policy)))
+    {
+        return false;
+    }
+    port->bus_name = nl_attr_get_string(attrs[DEVLINK_ATTR_BUS_NAME]);
+    port->dev_name = nl_attr_get_string(attrs[DEVLINK_ATTR_DEV_NAME]);
+    port->index = nl_attr_get_u32(attrs[DEVLINK_ATTR_PORT_INDEX]);
+
+    NL_UINT_ATTR_ASSIGN(port->type, DEVLINK_ATTR_PORT_TYPE,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->desired_type, DEVLINK_ATTR_PORT_DESIRED_TYPE,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->netdev_ifindex, DEVLINK_ATTR_PORT_NETDEV_IFINDEX,
+                        attrs, policy, ARRAY_SIZE(policy))
+    if (port->type == DEVLINK_PORT_TYPE_ETH &&
+            attrs[DEVLINK_ATTR_PORT_NETDEV_NAME])
+        port->netdev_name = nl_attr_get_string(
+            attrs[DEVLINK_ATTR_PORT_NETDEV_NAME]);
+    else if (port->type == DEVLINK_PORT_TYPE_IB &&
+            attrs[DEVLINK_ATTR_PORT_IBDEV_NAME])
+        port->ibdev_name = nl_attr_get_string(
+            attrs[DEVLINK_ATTR_PORT_IBDEV_NAME]);
+    else
+        port->netdev_name = "";
+    NL_UINT_ATTR_ASSIGN(port->split_count, DEVLINK_ATTR_PORT_SPLIT_COUNT,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->split_group, DEVLINK_ATTR_PORT_SPLIT_GROUP,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->flavour, DEVLINK_ATTR_PORT_FLAVOUR,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->number, DEVLINK_ATTR_PORT_NUMBER,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->split_subport_number,
+                        DEVLINK_ATTR_PORT_SPLIT_SUBPORT_NUMBER,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->pci_pf_number, DEVLINK_ATTR_PORT_PCI_PF_NUMBER,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->pci_vf_number, DEVLINK_ATTR_PORT_PCI_VF_NUMBER,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->lanes, DEVLINK_ATTR_PORT_LANES,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->splittable, DEVLINK_ATTR_PORT_SPLITTABLE,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->external, DEVLINK_ATTR_PORT_EXTERNAL,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->controller_number,
+                        DEVLINK_ATTR_PORT_CONTROLLER_NUMBER,
+                        attrs, policy, ARRAY_SIZE(policy))
+    NL_UINT_ATTR_ASSIGN(port->pci_sf_number, DEVLINK_ATTR_PORT_PCI_SF_NUMBER,
+                        attrs, policy, ARRAY_SIZE(policy))
+
+    if (attrs[DEVLINK_ATTR_PORT_FUNCTION]) {
+        if (!nl_dl_parse_port_function(attrs[DEVLINK_ATTR_PORT_FUNCTION],
+                                       &port->function))
+        {
+            return false;
+        }
+    } else {
+        memset(&port->function, 0, sizeof(port->function));
+        port->function.state = UINT8_MAX;
+        port->function.opstate = UINT8_MAX;
+    }
+
+    return true;
+}
+
+bool
+nl_dl_parse_info_version(struct nlattr *nla, struct dl_info_version *info_ver)
+{
+    static const struct nl_policy policy[] = {
+        /* Appeared in Linux v5.1 */
+        [DEVLINK_ATTR_INFO_VERSION_NAME] = { .type = NL_A_STRING,
+                                             .optional = true, },
+        [DEVLINK_ATTR_INFO_VERSION_VALUE] = { .type = NL_A_STRING,
+                                              .optional = true, },
+    };
+    struct nlattr *attrs[ARRAY_SIZE(policy)];
+    bool parsed;
+
+    parsed = nl_parse_nested(nla, policy, attrs, ARRAY_SIZE(policy));
+
+    if (parsed) {
+        NL_STR_ATTR_ASSIGN(info_ver->name, DEVLINK_ATTR_INFO_VERSION_NAME,
+                           attrs, policy, ARRAY_SIZE(policy))
+        NL_STR_ATTR_ASSIGN(info_ver->value, DEVLINK_ATTR_INFO_VERSION_NAME,
+                           attrs, policy, ARRAY_SIZE(policy))
+    }
+
+    return parsed;
+}
+
+bool
+nl_dl_parse_info_policy(struct ofpbuf *msg, struct dl_info *info)
+{
+    static const struct nl_policy policy[] = {
+        /* Appeared in Linux v5.1 */
+        [DEVLINK_ATTR_INFO_DRIVER_NAME] = { .type = NL_A_STRING,
+                                            .optional = false, },
+        [DEVLINK_ATTR_INFO_SERIAL_NUMBER] = { .type = NL_A_STRING,
+                                              .optional = true, },
+        [DEVLINK_ATTR_INFO_VERSION_FIXED] = { .type = NL_A_NESTED,
+                                              .optional = true, },
+        [DEVLINK_ATTR_INFO_VERSION_RUNNING] = { .type = NL_A_NESTED,
+                                                .optional = true, },
+        [DEVLINK_ATTR_INFO_VERSION_STORED] = { .type = NL_A_NESTED,
+                                               .optional = true, },
+
+        /* Appeared in Linux v5.9 */
+        [DEVLINK_ATTR_INFO_BOARD_SERIAL_NUMBER] = { .type = NL_A_STRING,
+                                                    .optional = true, },
+    };
+    struct nlattr *attrs[ARRAY_SIZE(policy)];
+
+    if (!nl_policy_parse(msg, NLMSG_HDRLEN + GENL_HDRLEN,
+                         policy, attrs,
+                         ARRAY_SIZE(policy)))
+    {
+        return false;
+    }
+    NL_STR_ATTR_ASSIGN(info->driver_name, DEVLINK_ATTR_INFO_DRIVER_NAME,
+                       attrs, policy, ARRAY_SIZE(policy))
+    NL_STR_ATTR_ASSIGN(info->serial_number, DEVLINK_ATTR_INFO_SERIAL_NUMBER,
+                       attrs, policy, ARRAY_SIZE(policy))
+    NL_STR_ATTR_ASSIGN(info->board_serial_number,
+                       DEVLINK_ATTR_INFO_BOARD_SERIAL_NUMBER,
+                       attrs, policy, ARRAY_SIZE(policy))
+    if (attrs[DEVLINK_ATTR_INFO_VERSION_FIXED]) {
+        if (!nl_dl_parse_info_version(attrs[DEVLINK_ATTR_INFO_VERSION_FIXED],
+                                      &info->version_fixed))
+        {
+            return false;
+        }
+    } else {
+        memset(&info->version_fixed, 0, sizeof(info->version_fixed));
+    }
+    if (attrs[DEVLINK_ATTR_INFO_VERSION_RUNNING]) {
+        if (!nl_dl_parse_info_version(attrs[DEVLINK_ATTR_INFO_VERSION_RUNNING],
+                                      &info->version_running))
+        {
+            return false;
+        }
+    } else {
+        memset(&info->version_running, 0, sizeof(info->version_running));
+    }
+    if (attrs[DEVLINK_ATTR_INFO_VERSION_STORED]) {
+        if (!nl_dl_parse_info_version(attrs[DEVLINK_ATTR_INFO_VERSION_STORED],
+                                      &info->version_stored))
+        {
+            return false;
+        }
+    } else {
+        memset(&info->version_stored, 0, sizeof(info->version_stored));
+    }
+    return true;
+}
+
+static int
+nl_devlink_init(void)
+{
+    static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
+    static int error;
+
+    if (ovsthread_once_start(&once)) {
+        error = nl_lookup_genl_family(DEVLINK_GENL_NAME, &ovs_devlink_family);
+        if (error) {
+            VLOG_INFO("Generic Netlink family '%s' does not exist. "
+                      "Linux version 4.6 or newer required.",
+                      DEVLINK_GENL_NAME);
+        }
+
+        ovsthread_once_done(&once);
+    }
+
+    return error;
+}
+
diff --git a/lib/netlink-devlink.h b/lib/netlink-devlink.h
new file mode 100644
index 000000000..299ff8bd2
--- /dev/null
+++ b/lib/netlink-devlink.h
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2021 Canonical
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef NETLINK_DEVLINK_H
+#define NETLINK_DEVLINK_H 1
+
+#include "netlink.h"
+#include "netlink-socket.h"
+
+struct dl_port_function {
+    struct eth_addr eth_addr; /* All zero may mean not supported by kernel */
+    struct ib_addr ib_addr; /* All zero may mean not supported by kernel */
+    uint8_t state;    /* UINT8_MAX = not supported by kernel */
+    uint8_t opstate;  /* UNIT8_MAX = not supported by kernel */
+};
+
+struct dl_port {
+    const char *bus_name;
+    const char *dev_name;
+    uint32_t index;
+    uint16_t type;
+    uint16_t desired_type;
+    uint32_t netdev_ifindex;
+    union {
+        const char *netdev_name; /* type DEVLINK_PORT_TYPE_ETH */
+        const char *ibdev_name;  /* type DEVLINK_PORT_TYPE_IB */
+    };
+    uint32_t split_count;
+    uint32_t split_group;
+    uint16_t flavour;
+    uint32_t number;
+    uint32_t split_subport_number;
+    uint16_t pci_pf_number;
+    uint16_t pci_vf_number;
+    struct dl_port_function function;
+    uint32_t lanes;
+    uint8_t splittable;
+    uint8_t external;
+    uint32_t controller_number;
+    uint32_t pci_sf_number;
+};
+
+struct dl_info_version {
+    const char *name;
+    const char *value;
+};
+
+struct dl_info {
+    const char *driver_name;
+    const char *serial_number;
+    const char *board_serial_number;
+    struct dl_info_version version_fixed;
+    struct dl_info_version version_running;
+    struct dl_info_version version_stored;
+};
+
+struct nl_dl_dump_state {
+    struct nl_dump dump;
+    struct ofpbuf buf;
+};
+
+void nl_msg_put_dlgenmsg(struct ofpbuf *, size_t, int, uint8_t, uint32_t);
+int nl_dl_dump_start(uint8_t, struct nl_dl_dump_state *);
+bool nl_dl_port_dump_next(struct nl_dl_dump_state *, struct dl_port *);
+bool nl_dl_info_dump_next(struct nl_dl_dump_state *, struct dl_info *);
+int nl_dl_dump_finish(struct nl_dl_dump_state *);
+bool nl_dl_parse_port_policy(struct ofpbuf *, struct dl_port *);
+bool nl_dl_parse_port_function(struct nlattr *, struct dl_port_function *);
+bool nl_dl_parse_info_policy(struct ofpbuf *, struct dl_info *);
+bool nl_dl_parse_info_version(struct nlattr *, struct dl_info_version *);
+
+/*
+ * Convenience macro for assigning unsigned integer types values.
+ *
+ * Call nl_policy_parse or nl_parse_nested first and then pass in destination
+ * variable name, attribute identifier along with attribute and policy arrays
+ * and their length. The macro will look up the type and call the respective
+ * helper to get the data.
+ *
+ * Non-presence of attribute is flagged by assigning the maximum value of the
+ * unsigned integer type.
+ */
+#define NL_UINT_ATTR_ASSIGN(DST, ATTR_IDX, ATTRS, POLICY, POLICY_LEN)        \
+    if (ATTR_IDX < POLICY_LEN && ATTRS[ATTR_IDX]) {                          \
+        switch(POLICY[ATTR_IDX].type) {                                      \
+        case NL_A_U8:                                                        \
+            DST = nl_attr_get_u8(ATTRS[ATTR_IDX]);                           \
+            break;                                                           \
+        case NL_A_U16:                                                       \
+            DST = nl_attr_get_u16(ATTRS[ATTR_IDX]);                          \
+            break;                                                           \
+        case NL_A_U32:                                                       \
+            DST = nl_attr_get_u32(ATTRS[ATTR_IDX]);                          \
+            break;                                                           \
+        case NL_A_U64:                                                       \
+            DST = nl_attr_get_u64(ATTRS[ATTR_IDX]);                          \
+            break;                                                           \
+        case NL_A_U128:                                                      \
+        case NL_A_STRING:                                                    \
+        case NL_A_NO_ATTR:                                                   \
+        case NL_A_UNSPEC:                                                    \
+        case NL_A_FLAG:                                                      \
+        case NL_A_IPV6:                                                      \
+        case NL_A_NESTED:                                                    \
+        case NL_A_HW_ADDR:                                                   \
+        case N_NL_ATTR_TYPES: default: OVS_NOT_REACHED();                    \
+        }                                                                    \
+    } else {                                                                 \
+        DST = -1;                                                            \
+    }
+
+/*
+ * Convenience macro for assigning pointer to strings.
+ *
+ * Call nl_policy_parse or nl_parse_nested first and then pass in destination
+ * variable name, attribute identifier along with attribute and policy arrays
+ * and their length. The macro will validate the type and call the respective
+ * helper to get the data.
+ *
+ * Non-presence of attribute is flagged by assigning a pointer to an empty
+ * string.
+ */
+#define NL_STR_ATTR_ASSIGN(DST, ATTR_IDX, ATTRS, POLICY, POLICY_LEN)         \
+    if (ATTR_IDX < POLICY_LEN && ATTRS[ATTR_IDX] &&                          \
+            POLICY[ATTR_IDX].type == NL_A_STRING) {                          \
+        DST = nl_attr_get_string(ATTRS[ATTR_IDX]);                           \
+    } else {                                                                 \
+        DST = "";                                                            \
+    }
+
+#endif /* NETLINK_DEVLINK_H */
diff --git a/lib/netlink.c b/lib/netlink.c
index 26ab20bb4..efc126528 100644
--- a/lib/netlink.c
+++ b/lib/netlink.c
@@ -741,6 +741,20 @@ nl_attr_get_nested(const struct nlattr *nla, struct ofpbuf *nested)
     ofpbuf_use_const(nested, nl_attr_get(nla), nl_attr_get_size(nla));
 }
 
+/* Returns the Ethernet Address value in 'nla''s payload. */
+struct eth_addr
+nl_attr_get_eth_addr(const struct nlattr *nla)
+{
+    return NL_ATTR_GET_AS(nla, struct eth_addr);
+}
+
+/* Returns the Infiniband Address value in 'nla''s payload. */
+struct ib_addr
+nl_attr_get_ib_addr(const struct nlattr *nla)
+{
+    return NL_ATTR_GET_AS(nla, struct ib_addr);
+}
+
 /* Default minimum payload size for each type of attribute. */
 static size_t
 min_attr_len(enum nl_attr_type type)
@@ -757,6 +771,7 @@ min_attr_len(enum nl_attr_type type)
     case NL_A_FLAG: return 0;
     case NL_A_IPV6: return 16;
     case NL_A_NESTED: return 0;
+    case NL_A_HW_ADDR: return 6;
     case N_NL_ATTR_TYPES: default: OVS_NOT_REACHED();
     }
 }
@@ -777,6 +792,7 @@ max_attr_len(enum nl_attr_type type)
     case NL_A_FLAG: return SIZE_MAX;
     case NL_A_IPV6: return 16;
     case NL_A_NESTED: return SIZE_MAX;
+    case NL_A_HW_ADDR: return 20;
     case N_NL_ATTR_TYPES: default: OVS_NOT_REACHED();
     }
 }
diff --git a/lib/netlink.h b/lib/netlink.h
index 44b8e4d1a..8410235fb 100644
--- a/lib/netlink.h
+++ b/lib/netlink.h
@@ -126,6 +126,8 @@ struct nlmsghdr *nl_msg_next(struct ofpbuf *buffer, struct ofpbuf *msg);
 #define NL_A_BE128_SIZE NL_ATTR_SIZE(sizeof(ovs_be128))
 #define NL_A_FLAG_SIZE NL_ATTR_SIZE(0)
 #define NL_A_IPV6_SIZE NL_ATTR_SIZE(sizeof(struct in6_addr))
+#define NL_A_HW_ADDR_ETH_SIZE NL_ATTR_SIZE(sizeof(struct eth_addr))
+#define NL_A_HW_ADDR_IB_SIZE NL_ATTR_SIZE(sizeof(struct ib_addr))
 
 bool nl_attr_oversized(size_t payload_size);
 
@@ -147,6 +149,7 @@ enum nl_attr_type
     NL_A_FLAG,
     NL_A_IPV6,
     NL_A_NESTED,
+    NL_A_HW_ADDR,
     N_NL_ATTR_TYPES
 };
 
@@ -214,6 +217,8 @@ struct in6_addr nl_attr_get_in6_addr(const struct nlattr *nla);
 odp_port_t nl_attr_get_odp_port(const struct nlattr *);
 const char *nl_attr_get_string(const struct nlattr *);
 void nl_attr_get_nested(const struct nlattr *, struct ofpbuf *);
+struct eth_addr nl_attr_get_eth_addr(const struct nlattr *nla);
+struct ib_addr nl_attr_get_ib_addr(const struct nlattr *nla);
 
 /* Netlink attribute policy.
  *
diff --git a/lib/packets.h b/lib/packets.h
index 481bc22fa..a5e1a3dc9 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -197,6 +197,7 @@ pkt_metadata_prefetch_init(struct pkt_metadata *md)
 bool dpid_from_string(const char *s, uint64_t *dpidp);
 
 #define ETH_ADDR_LEN           6
+#define IB_ADDR_LEN           20
 
 static const struct eth_addr eth_addr_broadcast OVS_UNUSED
     = ETH_ADDR_C(ff,ff,ff,ff,ff,ff);
diff --git a/utilities/.gitignore b/utilities/.gitignore
index 0a11356d4..414bb8058 100644
--- a/utilities/.gitignore
+++ b/utilities/.gitignore
@@ -1,5 +1,6 @@
 /Makefile
 /Makefile.in
+/devlink
 /nlmon
 /ovs-appctl
 /ovs-appctl.8
diff --git a/utilities/automake.mk b/utilities/automake.mk
index e2e22c39a..535936305 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -122,6 +122,11 @@ if LINUX
 noinst_PROGRAMS += utilities/nlmon
 utilities_nlmon_SOURCES = utilities/nlmon.c
 utilities_nlmon_LDADD = lib/libopenvswitch.la
+if DEVLINK
+noinst_PROGRAMS += utilities/devlink
+utilities_devlink_SOURCES = utilities/devlink.c
+utilities_devlink_LDADD = lib/libopenvswitch.la
+endif
 endif
 
 FLAKE8_PYFILES += utilities/ovs-pcap.in \
diff --git a/utilities/devlink.c b/utilities/devlink.c
new file mode 100644
index 000000000..855ccfd6c
--- /dev/null
+++ b/utilities/devlink.c
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2021 Canonical
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <config.h>
+#include <inttypes.h>
+#include <stddef.h>
+#include <linux/devlink.h>
+#include "netlink-devlink.h"
+#include "openvswitch/vlog.h"
+#include "packets.h"
+
+int
+main(int argc OVS_UNUSED, char *argv[])
+{
+    struct nl_dl_dump_state port_dump;
+    struct nl_dl_dump_state info_dump;
+    struct dl_port port_entry;
+    struct dl_info info_entry;
+    int error;
+
+    set_program_name(argv[0]);
+    vlog_set_levels(NULL, VLF_ANY_DESTINATION, VLL_DBG);
+
+    printf("port dump\n");
+    error = nl_dl_dump_start(DEVLINK_CMD_PORT_GET, &port_dump);
+    if (error) {
+        ovs_fatal(error, "error");
+    }
+    int i = 0;
+    while (nl_dl_port_dump_next(&port_dump, &port_entry)) {
+        printf("%d: %s\n", ++i, port_entry.bus_name);
+        printf("%d: %s\n", i, port_entry.dev_name);
+        printf("%d: %"PRIu32"\n", i, port_entry.index);
+        printf("%d: %"PRIu16"\n", i, port_entry.type);
+        printf("%d: %"PRIu16"\n", i, port_entry.desired_type);
+        printf("%d: %"PRIu32"\n", i, port_entry.netdev_ifindex);
+        printf("%d: %s\n", i, port_entry.netdev_name);
+        printf("%d: "ETH_ADDR_FMT"\n", i,
+               ETH_ADDR_ARGS(port_entry.function.eth_addr));
+        printf("%d: %"PRIu8"\n", i, port_entry.function.state);
+        printf("%d: %"PRIu8"\n", i, port_entry.function.opstate);
+    }
+    nl_dl_dump_finish(&port_dump);
+
+    printf("info dump\n");
+    error = nl_dl_dump_start(DEVLINK_CMD_INFO_GET, &info_dump);
+    if (error) {
+        ovs_fatal(error, "error");
+    }
+    i = 0;
+    while (nl_dl_info_dump_next(&info_dump, &info_entry)) {
+        printf("%d: %s\n", ++i, info_entry.driver_name);
+    }
+    nl_dl_dump_finish(&info_dump);
+}
-- 
2.30.2




More information about the dev mailing list