diff --git a/MAINTAINERS b/MAINTAINERS
index abbe88e1c50b..295a6b0fbe26 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15434,6 +15434,7 @@ S:	Maintained
 W:	http://openvswitch.org
 F:	include/uapi/linux/openvswitch.h
 F:	net/openvswitch/
+F:	tools/testing/selftests/net/openvswitch/
 
 OPERATING PERFORMANCE POINTS (OPP)
 M:	Viresh Kumar <vireshk@kernel.org>
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 0464b2c6c1e4..f07aef7c592c 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -49,6 +49,7 @@ TARGETS += net
 TARGETS += net/af_unix
 TARGETS += net/forwarding
 TARGETS += net/mptcp
+TARGETS += net/openvswitch
 TARGETS += netfilter
 TARGETS += nsfs
 TARGETS += pidfd
diff --git a/tools/testing/selftests/net/openvswitch/Makefile b/tools/testing/selftests/net/openvswitch/Makefile
new file mode 100644
index 000000000000..2f1508abc826
--- /dev/null
+++ b/tools/testing/selftests/net/openvswitch/Makefile
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0
+
+top_srcdir = ../../../../..
+
+CFLAGS =  -Wall -Wl,--no-as-needed -O2 -g -I$(top_srcdir)/usr/include $(KHDR_INCLUDES)
+
+TEST_PROGS := openvswitch.sh
+
+TEST_FILES := ovs-dpctl.py
+
+EXTRA_CLEAN := test_netlink_checks
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools/testing/selftests/net/openvswitch/openvswitch.sh
new file mode 100755
index 000000000000..bebc20f157dc
--- /dev/null
+++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh
@@ -0,0 +1,216 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+#
+# OVS kernel module self tests
+
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
+
+PAUSE_ON_FAIL=no
+VERBOSE=0
+TRACING=0
+
+tests="
+	netlink_checks				ovsnl: validate netlink attrs and settings"
+
+info() {
+    [ $VERBOSE = 0 ] || echo $*
+}
+
+ovs_base=`pwd`
+sbxs=
+sbx_add () {
+	info "adding sandbox '$1'"
+
+	sbxs="$sbxs $1"
+
+	NO_BIN=0
+
+	# Create sandbox.
+	local d="$ovs_base"/$1
+	if [ -e $d ]; then
+		info "removing $d"
+		rm -rf "$d"
+	fi
+	mkdir "$d" || return 1
+	ovs_setenv $1
+}
+
+ovs_exit_sig() {
+	[ -e ${ovs_dir}/cleanup ] && . "$ovs_dir/cleanup"
+}
+
+on_exit() {
+	echo "$1" > ${ovs_dir}/cleanup.tmp
+	cat ${ovs_dir}/cleanup >> ${ovs_dir}/cleanup.tmp
+	mv ${ovs_dir}/cleanup.tmp ${ovs_dir}/cleanup
+}
+
+ovs_setenv() {
+	sandbox=$1
+
+	ovs_dir=$ovs_base${1:+/$1}; export ovs_dir
+
+	test -e ${ovs_dir}/cleanup || : > ${ovs_dir}/cleanup
+}
+
+ovs_sbx() {
+	if test "X$2" != X; then
+		(ovs_setenv $1; shift; "$@" >> ${ovs_dir}/debug.log)
+	else
+		ovs_setenv $1
+	fi
+}
+
+ovs_add_dp () {
+	info "Adding DP/Bridge IF: sbx:$1 dp:$2 {$3, $4, $5}"
+	ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py add-dp "$2" "$3" "$4" "$5" || return 1
+	on_exit "ovs_sbx $1 python3 $ovs_base/ovs-dpctl.py del-dp $2;"
+}
+
+usage() {
+	echo
+	echo "$0 [OPTIONS] [TEST]..."
+	echo "If no TEST argument is given, all tests will be run."
+	echo
+	echo "Options"
+	echo "  -t: capture traffic via tcpdump"
+	echo "  -v: verbose"
+	echo "  -p: pause on failure"
+	echo
+	echo "Available tests${tests}"
+	exit 1
+}
+
+# netlink_validation
+# - Create a dp
+# - check no warning with "old version" simulation
+test_netlink_checks () {
+	sbx_add "test_netlink_checks" || return 1
+
+	info "setting up new DP"
+	ovs_add_dp "test_netlink_checks" nv0 || return 1
+	# now try again
+	PRE_TEST=$(dmesg | grep -E "RIP: [0-9a-fA-Fx]+:ovs_dp_cmd_new\+")
+	ovs_add_dp "test_netlink_checks" nv0 -V 0 || return 1
+	POST_TEST=$(dmesg | grep -E "RIP: [0-9a-fA-Fx]+:ovs_dp_cmd_new\+")
+	if [ "$PRE_TEST" != "$POST_TEST" ]; then
+		info "failed - gen warning"
+		return 1
+	fi
+
+	return 0
+}
+
+run_test() {
+	(
+	tname="$1"
+	tdesc="$2"
+
+	if ! lsmod | grep openvswitch >/dev/null 2>&1; then
+		stdbuf -o0 printf "TEST: %-60s  [NOMOD]\n" "${tdesc}"
+		return $ksft_skip
+	fi
+
+	if python3 ovs-dpctl.py help 2>&1 | \
+	     grep "Need to install the python" >/dev/null 2>&1; then
+		stdbuf -o0 printf "TEST: %-60s  [PYLIB]\n" "${tdesc}"
+		return $ksft_skip
+	fi
+	printf "TEST: %-60s  [START]\n" "${tname}"
+
+	unset IFS
+
+	eval test_${tname}
+	ret=$?
+
+	if [ $ret -eq 0 ]; then
+		printf "TEST: %-60s  [ OK ]\n" "${tdesc}"
+		ovs_exit_sig
+		rm -rf "$ovs_dir"
+	elif [ $ret -eq 1 ]; then
+		printf "TEST: %-60s  [FAIL]\n" "${tdesc}"
+		if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
+			echo
+			echo "Pausing. Logs in $ovs_dir/. Hit enter to continue"
+			read a
+		fi
+		ovs_exit_sig
+		[ "${PAUSE_ON_FAIL}" = "yes" ] || rm -rf "$ovs_dir"
+		exit 1
+	elif [ $ret -eq $ksft_skip ]; then
+		printf "TEST: %-60s  [SKIP]\n" "${tdesc}"
+	elif [ $ret -eq 2 ]; then
+		rm -rf test_${tname}
+		run_test "$1" "$2"
+	fi
+
+	return $ret
+	)
+	ret=$?
+	case $ret in
+		0)
+			[ $all_skipped = true ] && [ $exitcode=$ksft_skip ] && exitcode=0
+			all_skipped=false
+		;;
+		$ksft_skip)
+			[ $all_skipped = true ] && exitcode=$ksft_skip
+		;;
+		*)
+			all_skipped=false
+			exitcode=1
+		;;
+	esac
+
+	return $ret
+}
+
+
+exitcode=0
+desc=0
+all_skipped=true
+
+while getopts :pvt o
+do
+	case $o in
+	p) PAUSE_ON_FAIL=yes;;
+	v) VERBOSE=1;;
+	t) if which tcpdump > /dev/null 2>&1; then
+		TRACING=1
+	   else
+		echo "=== tcpdump not available, tracing disabled"
+	   fi
+	   ;;
+	*) usage;;
+	esac
+done
+shift $(($OPTIND-1))
+
+IFS="	
+"
+
+for arg do
+	# Check first that all requested tests are available before running any
+	command -v > /dev/null "test_${arg}" || { echo "=== Test ${arg} not found"; usage; }
+done
+
+name=""
+desc=""
+for t in ${tests}; do
+	[ "${name}" = "" ]	&& name="${t}"	&& continue
+	[ "${desc}" = "" ]	&& desc="${t}"
+
+	run_this=1
+	for arg do
+		[ "${arg}" != "${arg#--*}" ] && continue
+		[ "${arg}" = "${name}" ] && run_this=1 && break
+		run_this=0
+	done
+	if [ $run_this -eq 1 ]; then
+		run_test "${name}" "${desc}"
+	fi
+	name=""
+	desc=""
+done
+
+exit ${exitcode}
diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
new file mode 100644
index 000000000000..791d76b7adcd
--- /dev/null
+++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
@@ -0,0 +1,411 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+# Controls the openvswitch module.  Part of the kselftest suite, but
+# can be used for some diagnostic purpose as well.
+
+import logging
+import multiprocessing
+import socket
+import struct
+import sys
+
+try:
+    from libnl.attr import NLA_NESTED, NLA_STRING, NLA_U32, NLA_UNSPEC
+    from libnl.attr import nla_get_string, nla_get_u32
+    from libnl.attr import nla_put, nla_put_string, nla_put_u32
+    from libnl.attr import nla_policy
+
+    from libnl.error import errmsg
+
+    from libnl.genl.ctrl import genl_ctrl_resolve
+    from libnl.genl.genl import genl_connect, genlmsg_parse, genlmsg_put
+
+    from libnl.handlers import nl_cb_alloc, nl_cb_set
+    from libnl.handlers import NL_CB_CUSTOM, NL_CB_MSG_IN, NL_CB_VALID
+    from libnl.handlers import NL_OK, NL_STOP
+
+    from libnl.linux_private.netlink import NLM_F_ACK, NLM_F_DUMP
+    from libnl.linux_private.netlink import NLM_F_REQUEST, NLMSG_DONE
+
+    from libnl.msg import NL_AUTO_SEQ, nlmsg_alloc, nlmsg_hdr
+
+    from libnl.nl import NLMSG_ERROR, nl_recvmsgs_default, nl_send_auto
+    from libnl.socket_ import nl_socket_alloc, nl_socket_set_cb
+    from libnl.socket_ import nl_socket_get_local_port
+except ModuleNotFoundError:
+    print("Need to install the python libnl3 library.")
+    print("Exiting without error.")
+    exit(0)
+
+
+global sk
+global ovs_families
+
+OVS_DATAPATH_FAMILY = "ovs_datapath"
+OVS_VPORT_FAMILY = "ovs_vport"
+OVS_FLOW_FAMILY = "ovs_flow"
+OVS_PACKET_FAMILY = "ovs_packet"
+OVS_METER_FAMILY = "ovs_meter"
+OVS_CT_LIMIT_FAMILY = "ovs_ct_limit"
+
+OVS_DATAPATH_VERSION = 2
+OVS_HDR_LEN = 4
+OVS_DP_CMD_NEW = 1
+OVS_DP_CMD_DEL = 2
+OVS_DP_CMD_GET = 3
+OVS_DP_CMD_SET = 4
+
+OVS_DP_F_VPORT_PIDS = 1 << 1
+OVS_DP_F_DISPATCH_UPCALL_PER_CPU = 1 << 3
+
+OVS_DP_ATTR_NAME = 1
+OVS_DP_ATTR_UPCALL_PID = 2
+OVS_DP_ATTR_STATS = 3
+OVS_DP_ATTR_MEGAFLOW_STATS = 4
+OVS_DP_ATTR_USER_FEATURES = 5
+OVS_DP_ATTR_PAD = 6
+OVS_DP_ATTR_MASKS_CACHE_SIZE = 7
+OVS_DP_ATTR_PER_CPU_PIDS = 8
+OVS_DP_ATTR_MAX = 8
+
+OVS_VPORT_CMD_NEW = 1
+OVS_VPORT_CMD_DEL = 2
+OVS_VPORT_CMD_GET = 3
+OVS_VPORT_CMD_SET = 4
+
+OVS_VPORT_ATTR_PORT_NO = 1
+OVS_VPORT_ATTR_TYPE = 2
+OVS_VPORT_ATTR_NAME = 3
+OVS_VPORT_ATTR_OPTIONS = 4
+OVS_VPORT_ATTR_UPCALL_PID = 5
+OVS_VPORT_ATTR_STATS = 6
+OVS_VPORT_ATTR_PAD = 7
+OVS_VPORT_ATTR_IFINDEX = 8
+OVS_VPORT_ATTR_NETNSID = 9
+OVS_VPORT_ATTR_MAX = 9
+
+OVS_VPORT_TYPE_NETDEV = 1
+OVS_VPORT_TYPE_INTERNAL = 2
+OVS_VPORT_TYPE_GRE = 3
+OVS_VPORT_TYPE_VXLAN = 4
+OVS_VPORT_TYPE_GENEVE = 5
+OVS_VPORT_TYPE_MAX = 5
+
+
+def nl_sk_transaction(msg, sk, cb):
+    nl_socket_set_cb(sk, cb)
+    ret = nl_send_auto(sk, msg)
+    if ret < 0:
+        print("send error: ", end='')
+        print(errmsg[abs(ret)])
+    ret = nl_recvmsgs_default(sk)
+    if ret < 0:
+        print("recv error: ", end='')
+        print(errmsg[abs(ret)])
+    return ret
+
+
+def if_exists(ifname):
+    try:
+        socket.if_nametoindex(ifname)
+        return True
+    except OSError:
+        return False
+
+
+def get_family(ovs_family_name):
+    """
+    Retrieve a GENL family ID via the global nl socket
+    Returns: family ID for the requested family name
+    """
+    global sk
+    if sk is None:
+        raise ConnectionError("sk not correctly setup")
+    numid = genl_ctrl_resolve(sk, ovs_family_name.encode('utf-8'))
+    return numid
+
+
+def dpctl_netlink_init():
+    """
+    Initializes the global netlink socket, and ovs familly dictionary
+    Returns: 0 on success, any other value is error
+    """
+    global sk, ovs_families
+    sk = nl_socket_alloc()
+    ret = genl_connect(sk)
+    if ret:
+        print(errmsg[abs(ret)])
+        sk = None
+        return ret
+    ovs_families = {}
+    family_probe = [OVS_DATAPATH_FAMILY, OVS_VPORT_FAMILY, OVS_FLOW_FAMILY,
+                    OVS_PACKET_FAMILY, OVS_METER_FAMILY, OVS_CT_LIMIT_FAMILY]
+    for family in family_probe:
+        ovs_families[family] = get_family(family)
+        if ovs_families[family] == -1:
+            return -1
+    return 0
+
+
+def parse_dp_msg(nlh, target_dict):
+    dp_dict = {}
+    attrs = dict((i, None) for i in range(OVS_DP_ATTR_MAX))
+    dp_policy = dict((i, None) for i in range(OVS_DP_ATTR_MAX))
+    dp_policy.update({
+        OVS_DP_ATTR_NAME: nla_policy(type_=NLA_STRING, maxlen=15),
+        OVS_DP_ATTR_UPCALL_PID: nla_policy(type_=NLA_U32),
+        OVS_DP_ATTR_STATS: nla_policy(type_=NLA_NESTED),
+        OVS_DP_ATTR_MEGAFLOW_STATS: nla_policy(type_=NLA_NESTED),
+        OVS_DP_ATTR_USER_FEATURES: nla_policy(type_=NLA_U32),
+        OVS_DP_ATTR_MASKS_CACHE_SIZE: nla_policy(type_=NLA_U32),
+        OVS_DP_ATTR_PER_CPU_PIDS: nla_policy(type_=NLA_UNSPEC)
+    })
+    ret = genlmsg_parse(nlh, 4, attrs, OVS_DP_ATTR_MAX, dp_policy)
+    if ret:
+        print("Error parsing datapath")
+        return -1
+    if attrs[1] is None:
+        print("Error?")
+    dp_name = nla_get_string(attrs[1]).decode('utf-8')
+    b = bytes(attrs[OVS_DP_ATTR_STATS].payload)
+    stats = struct.unpack("=QQQQ", b[:32])
+    dp_dict[OVS_DP_ATTR_STATS] = stats
+    b = bytes(attrs[OVS_DP_ATTR_MEGAFLOW_STATS].payload)
+    stats = struct.unpack("=QIIQQ", b[:32])
+    dp_dict[OVS_DP_ATTR_MEGAFLOW_STATS] = [stats[i] for i in (0, 1, 3)]
+    dp_dict[OVS_DP_ATTR_MASKS_CACHE_SIZE] = nla_get_u32(
+        attrs[OVS_DP_ATTR_MASKS_CACHE_SIZE])
+    target_dict[dp_name] = dp_dict
+
+
+def show_dp_cb(msg, dp_dict):
+    nlh = nlmsg_hdr(msg)
+    if nlh.nlmsg_type == NLMSG_DONE:
+        retn = NL_STOP
+    parse_dp_msg(nlh, dp_dict)
+    retn = NL_OK
+    return retn
+
+
+def show_vport_cb(msg, dp_vport_dict):
+    dp, vport_dict = dp_vport_dict
+    nlh = nlmsg_hdr(msg)
+    retn = None
+    if nlh.nlmsg_type == NLMSG_DONE:
+        retn = NL_STOP
+    attrs = dict((i, None) for i in range(OVS_DP_ATTR_MAX))
+    port_policy = dict((i, None) for i in range(OVS_VPORT_ATTR_MAX))
+    port_policy.update({
+            OVS_VPORT_ATTR_PORT_NO: nla_policy(type_=NLA_U32),
+            OVS_VPORT_ATTR_TYPE: nla_policy(type_=NLA_U32),
+            OVS_VPORT_ATTR_NAME: nla_policy(type_=NLA_STRING, maxlen=15),
+            OVS_VPORT_ATTR_OPTIONS: nla_policy(type_=NLA_NESTED),
+            OVS_VPORT_ATTR_UPCALL_PID: nla_policy(type_=NLA_UNSPEC),
+            OVS_VPORT_ATTR_STATS: nla_policy(type_=NLA_NESTED),
+            OVS_VPORT_ATTR_IFINDEX: nla_policy(type_=NLA_U32),
+        })
+    genlmsg_parse(nlh, OVS_HDR_LEN, attrs, OVS_DP_ATTR_MAX, port_policy)
+    if attrs[1] is not None:
+        port_info = "Port " + str(nla_get_u32(attrs[1])) + ": "
+        if attrs[3] is not None:
+            port_info += nla_get_string(attrs[3]).decode('utf-8')
+            if attrs[OVS_VPORT_ATTR_TYPE] is not None:
+                port_type = nla_get_u32(attrs[OVS_VPORT_ATTR_TYPE])
+                if port_type == OVS_VPORT_TYPE_INTERNAL:
+                    port_info += " (internal)"
+    vport_dict[dp].append(port_info)
+    if retn is None:
+        retn = NL_OK
+    return retn
+
+
+def dpctl_show_print(dp_info, vport_info):
+    for i in dp_info:
+        print("{}".format(i))
+        indent = 2 * " "
+        fields = ("Hit", "Missed", "Lost", "Flows")
+        f_zip = zip(fields, dp_info[i][OVS_DP_ATTR_STATS])
+        format_list = [val for pair in f_zip for val in pair]
+        out_string = indent + "Lookups: {}: {} {}: {} {}: {}\n"
+        out_string += indent + "{}: {}"
+        print(out_string.format(*format_list))
+        fields = ("Hit", "Total", "Hit")
+        f_zip = zip(fields, dp_info[i][OVS_DP_ATTR_MEGAFLOW_STATS])
+        format_list = [val for pair in f_zip for val in pair]
+        out_string = indent + "Masks: {}: {} {}: {}\n"
+        out_string += indent + "Cache: {}: {}"
+        print(out_string.format(*format_list))
+        print("Caches:\n" + indent + "Masks-cache: size: {}".
+              format(dp_info[i][OVS_DP_ATTR_MASKS_CACHE_SIZE]))
+        indent = 4 * " "
+        for port in vport_info[i]:
+            print(indent + port)
+
+
+def dpctl_show(dp=None):
+    global sk, ovs_families
+    cb_dp_show = nl_cb_alloc(NL_CB_CUSTOM)
+    dp_info = {}
+    vport_info = {}
+    nl_cb_set(cb_dp_show, NL_CB_VALID, NL_CB_CUSTOM, show_dp_cb, dp_info)
+    msg_dpctl_get = nlmsg_alloc()
+    if dp is not None:
+        if not if_exists(dp):
+            print("That interface does not exist.")
+            return -1
+        flag = NLM_F_REQUEST
+    else:
+        flag = NLM_F_DUMP
+    genlmsg_put(msg_dpctl_get, 0, NL_AUTO_SEQ,
+                ovs_families[OVS_DATAPATH_FAMILY], OVS_HDR_LEN,
+                flag, OVS_DP_CMD_GET, OVS_DATAPATH_VERSION)
+    if dp is not None:
+        nla_put_string(msg_dpctl_get, OVS_DP_ATTR_NAME, dp.encode('utf-8'))
+    nl_sk_transaction(msg_dpctl_get, sk, cb_dp_show)
+    vport_info = dict((i, []) for i in dp_info)
+    # for each datapath, call down and ask it to tell us its vports.
+    for dp in vport_info:
+        msg_vport_get = nlmsg_alloc()
+        ba = genlmsg_put(msg_vport_get, 0, NL_AUTO_SEQ,
+                         ovs_families[OVS_VPORT_FAMILY], OVS_HDR_LEN,
+                         NLM_F_DUMP, OVS_VPORT_CMD_GET, OVS_DATAPATH_VERSION)
+        ba[0:OVS_HDR_LEN] = struct.pack('=I', socket.if_nametoindex(dp))
+        cb_vport_show = nl_cb_alloc(NL_CB_CUSTOM)
+        nl_cb_set(cb_vport_show, NL_CB_VALID, NL_CB_CUSTOM,
+                  show_vport_cb, (dp, vport_info))
+        nl_sk_transaction(msg_vport_get, sk, cb_vport_show)
+    dpctl_show_print(dp_info, vport_info)
+
+
+def mod_cb(msg, add):
+    nlh = nlmsg_hdr(msg)
+    if nlh.nlmsg_type == NLMSG_ERROR:
+        b = nlh.payload
+        s = struct.unpack('=i', b[:4])[0]
+        if s:
+            print(errmsg[abs(s)])
+            return NL_STOP
+    action = "added" if add else "deleted"
+    print("Successfully {} the datapath.".format(action))
+    return NL_OK
+
+
+def dpctl_mod_dp(args, add=True, setpid=False, hdrval=None):
+    global ovs_families, sk
+
+    dp = args[0]
+    cmd = OVS_DP_CMD_NEW if add else OVS_DP_CMD_DEL
+    msg_dpctl_cmd = nlmsg_alloc()
+
+    userfeatures = 0
+    if hdrval is None:
+        hdrver = OVS_DATAPATH_VERSION
+        userfeatures = OVS_DP_F_VPORT_PIDS
+    else:
+        segment = hdrval.find(":")
+        if segment == -1:
+            segment = len(hdrval)
+        hdrver = int(hdrval[:segment], 0)
+        if len(hdrval[:segment]):
+            userfeatures = int(hdrval[:segment], 0)
+
+    genlmsg_put(msg_dpctl_cmd, 0, NL_AUTO_SEQ,
+                ovs_families[OVS_DATAPATH_FAMILY], OVS_HDR_LEN,
+                NLM_F_ACK, cmd, hdrver)
+
+    nla_put_u32(msg_dpctl_cmd, OVS_DP_ATTR_UPCALL_PID, 0)
+    nla_put_string(msg_dpctl_cmd, OVS_DP_ATTR_NAME, dp.encode('utf-8'))
+
+    if setpid:
+        userfeatures &= ~OVS_DP_F_VPORT_PIDS
+        userfeatures |= OVS_DP_F_DISPATCH_UPCALL_PER_CPU
+        procarray = None
+        nproc = multiprocessing.cpu_count()
+        for i in range(nproc):
+            if procarray is not None:
+                procarray += struct.pack("=I", nl_socket_get_local_port(sk))
+            else:
+                procarray = struct.pack('=I', nl_socket_get_local_port(sk))
+        nla_put(msg_dpctl_cmd, OVS_DP_ATTR_UPCALL_PID, len(procarray),
+                procarray)
+    nla_put_u32(msg_dpctl_cmd, OVS_DP_ATTR_USER_FEATURES, userfeatures)
+    cb_dp_mod = nl_cb_alloc(NL_CB_CUSTOM)
+    nl_cb_set(cb_dp_mod, NL_CB_MSG_IN, NL_CB_CUSTOM, mod_cb, add)
+    return nl_sk_transaction(msg_dpctl_cmd, sk, cb_dp_mod)
+
+
+def dpctl_add_dp(dp):
+    setpid = False
+    dphdr = None
+    if len(dp) > 1:
+        for i in range(len(dp)):
+            if dp[i] == '-u':
+                setpid = True
+            elif dp[i] == '-V':
+                i += 1
+                dphdr = dp[i]
+
+    return dpctl_mod_dp(dp, True, setpid, dphdr)
+
+
+def dpctl_del_dp(dp):
+    args = [dp]
+    return dpctl_mod_dp(args, False)
+
+
+def help(errStr=None):
+    """
+    Display a help message, include errStr if there was an error.
+    Return: None
+    """
+    if errStr is None:
+        print("ovs-dpctl.py: openvswitch module controller")
+    else:
+        print(errStr)
+    print("usage:")
+    print("  show [DP]\t\t\tDispay information about all datapaths, or DP")
+    print("  add-dp DP\t\t\tAdd new datapath DP")
+    print("  del-dp DP\t\t\tDelete local datapath DP")
+
+
+def main(argv):
+    if len(argv) < 2:
+        help()
+        return 0
+    count = 1
+    for arg in argv[1:]:
+        count += 1
+        if arg in ("-v", "--verbose"):
+            logging.basicConfig(level=logging.DEBUG)
+        if arg in ("-h", "--help", "help"):
+            help()
+            return 0
+        if arg == "show":
+            dpctl_netlink_init()
+            if len(argv) <= count:
+                dpctl_show()
+            else:
+                dpctl_show(argv[count])
+            return 0
+        elif arg == "add-dp":
+            dpctl_netlink_init()
+            if len(argv) < 3:   # 3rd arg should be DP name or additional opts
+                help("Missing a DP name")
+                return -1
+            else:
+                dpctl_add_dp(argv[count:])
+            return 0
+        elif arg == "del-dp":
+            dpctl_netlink_init()
+            if len(argv) < 3:   # 3rd arg MUST be DP name
+                help("Missing a DP name")
+                return -1
+            else:
+                dpctl_del_dp(argv[count])
+            return 0
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
