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);
+}
