dpif-netlink: add GENEVE creation support
[cascardo/ovs.git] / lib / dpif-netlink.c
index 6ac973e..c99d7da 100644 (file)
 #include <inttypes.h>
 #include <net/if.h>
 #include <linux/types.h>
+#include <linux/ip.h>
+#include <linux/if_tunnel.h>
 #include <linux/pkt_sched.h>
+#include <linux/rtnetlink.h>
 #include <poll.h>
 #include <stdlib.h>
 #include <strings.h>
@@ -45,7 +48,7 @@
 #include "netlink-socket.h"
 #include "netlink.h"
 #include "odp-util.h"
-#include "ofpbuf.h"
+#include "openvswitch/ofpbuf.h"
 #include "packets.h"
 #include "poll-loop.h"
 #include "random.h"
@@ -780,10 +783,8 @@ get_vport_type(const struct dpif_netlink_vport *vport)
 }
 
 static enum ovs_vport_type
-netdev_to_ovs_vport_type(const struct netdev *netdev)
+netdev_to_ovs_vport_type(const char *type)
 {
-    const char *type = netdev_get_type(netdev);
-
     if (!strcmp(type, "tap") || !strcmp(type, "system")) {
         return OVS_VPORT_TYPE_NETDEV;
     } else if (!strcmp(type, "internal")) {
@@ -804,19 +805,14 @@ netdev_to_ovs_vport_type(const struct netdev *netdev)
 }
 
 static int
-dpif_netlink_port_add__(struct dpif_netlink *dpif, struct netdev *netdev,
+dpif_netlink_port_add__(struct dpif_netlink *dpif, const char *name,
+                        enum ovs_vport_type type,
+                        struct ofpbuf *options,
                         odp_port_t *port_nop)
     OVS_REQ_WRLOCK(dpif->upcall_lock)
 {
-    const struct netdev_tunnel_config *tnl_cfg;
-    char namebuf[NETDEV_VPORT_NAME_BUFSIZE];
-    const char *name = netdev_vport_get_dpif_port(netdev,
-                                                  namebuf, sizeof namebuf);
-    const char *type = netdev_get_type(netdev);
     struct dpif_netlink_vport request, reply;
     struct ofpbuf *buf;
-    uint64_t options_stub[64 / 8];
-    struct ofpbuf options;
     struct nl_sock **socksp = NULL;
     uint32_t *upcall_pids;
     int error = 0;
@@ -831,17 +827,80 @@ dpif_netlink_port_add__(struct dpif_netlink *dpif, struct netdev *netdev,
     dpif_netlink_vport_init(&request);
     request.cmd = OVS_VPORT_CMD_NEW;
     request.dp_ifindex = dpif->dp_ifindex;
-    request.type = netdev_to_ovs_vport_type(netdev);
-    if (request.type == OVS_VPORT_TYPE_UNSPEC) {
+    request.type = type;
+    request.name = name;
+
+    request.port_no = *port_nop;
+    upcall_pids = vport_socksp_to_pids(socksp, dpif->n_handlers);
+    request.n_upcall_pids = socksp ? dpif->n_handlers : 1;
+    request.upcall_pids = upcall_pids;
+
+    if (options) {
+        request.options = options->data;
+        request.options_len = options->size;
+    }
+
+    error = dpif_netlink_vport_transact(&request, &reply, &buf);
+    if (!error) {
+        *port_nop = reply.port_no;
+    } else {
+        if (error == EBUSY && *port_nop != ODPP_NONE) {
+            VLOG_INFO("%s: requested port %"PRIu32" is in use",
+                      dpif_name(&dpif->dpif), *port_nop);
+        }
+
+        vport_del_socksp(dpif, socksp);
+        goto exit;
+    }
+
+    if (socksp) {
+        error = vport_add_channels(dpif, *port_nop, socksp);
+        if (error) {
+            VLOG_INFO("%s: could not add channel for port %s",
+                      dpif_name(&dpif->dpif), name);
+
+            /* Delete the port. */
+            dpif_netlink_vport_init(&request);
+            request.cmd = OVS_VPORT_CMD_DEL;
+            request.dp_ifindex = dpif->dp_ifindex;
+            request.port_no = *port_nop;
+            dpif_netlink_vport_transact(&request, NULL, NULL);
+            vport_del_socksp(dpif, socksp);
+            goto exit;
+        }
+    }
+    free(socksp);
+
+exit:
+    ofpbuf_delete(buf);
+    free(upcall_pids);
+
+    return error;
+}
+
+static int
+dpif_netlink_port_add_compat(struct dpif_netlink *dpif, struct netdev *netdev,
+                             odp_port_t *port_nop)
+    OVS_REQ_WRLOCK(dpif->upcall_lock)
+{
+    const struct netdev_tunnel_config *tnl_cfg;
+    char namebuf[NETDEV_VPORT_NAME_BUFSIZE];
+    const char *name = netdev_vport_get_dpif_port(netdev,
+                                                  namebuf, sizeof namebuf);
+    const char *type = netdev_get_type(netdev);
+    uint64_t options_stub[64 / 8];
+    struct ofpbuf options;
+    enum ovs_vport_type ovs_type;
+
+    ovs_type = netdev_to_ovs_vport_type(netdev_get_type(netdev));
+    if (ovs_type == OVS_VPORT_TYPE_UNSPEC) {
         VLOG_WARN_RL(&error_rl, "%s: cannot create port `%s' because it has "
                      "unsupported type `%s'",
                      dpif_name(&dpif->dpif), name, type);
-        vport_del_socksp(dpif, socksp);
         return EINVAL;
     }
-    request.name = name;
 
-    if (request.type == OVS_VPORT_TYPE_NETDEV) {
+    if (ovs_type == OVS_VPORT_TYPE_NETDEV) {
 #ifdef _WIN32
         /* XXX : Map appropiate Windows handle */
 #else
@@ -868,50 +927,406 @@ dpif_netlink_port_add__(struct dpif_netlink *dpif, struct netdev *netdev,
             }
             nl_msg_end_nested(&options, ext_ofs);
         }
-        request.options = options.data;
-        request.options_len = options.size;
+        return dpif_netlink_port_add__(dpif, name, ovs_type, &options, port_nop);
+    } else {
+        return dpif_netlink_port_add__(dpif, name, ovs_type, NULL, port_nop);
     }
 
-    request.port_no = *port_nop;
-    upcall_pids = vport_socksp_to_pids(socksp, dpif->n_handlers);
-    request.n_upcall_pids = socksp ? dpif->n_handlers : 1;
-    request.upcall_pids = upcall_pids;
+}
 
-    error = dpif_netlink_vport_transact(&request, &reply, &buf);
-    if (!error) {
-        *port_nop = reply.port_no;
-    } else {
-        if (error == EBUSY && *port_nop != ODPP_NONE) {
-            VLOG_INFO("%s: requested port %"PRIu32" is in use",
-                      dpif_name(&dpif->dpif), *port_nop);
-        }
+#ifdef __linux__
 
-        vport_del_socksp(dpif, socksp);
-        goto exit;
+static int
+netdev_linux_destroy(const char *name)
+{
+    int err;
+    struct ofpbuf request, *reply;
+
+    ofpbuf_init(&request, 0);
+    nl_msg_put_nlmsghdr(&request, 0, RTM_DELLINK,
+                        NLM_F_REQUEST | NLM_F_ACK);
+    ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    nl_msg_put_string(&request, IFLA_IFNAME, name);
+
+    err = nl_transact(NETLINK_ROUTE, &request, &reply);
+
+    if (!err) {
+        ofpbuf_uninit(reply);
     }
 
-    if (socksp) {
-        error = vport_add_channels(dpif, *port_nop, socksp);
-        if (error) {
-            VLOG_INFO("%s: could not add channel for port %s",
-                      dpif_name(&dpif->dpif), name);
+    ofpbuf_uninit(&request);
+    return err;
+}
 
-            /* Delete the port. */
-            dpif_netlink_vport_init(&request);
-            request.cmd = OVS_VPORT_CMD_DEL;
-            request.dp_ifindex = dpif->dp_ifindex;
-            request.port_no = *port_nop;
-            dpif_netlink_vport_transact(&request, NULL, NULL);
-            vport_del_socksp(dpif, socksp);
-            goto exit;
+static int
+netdev_vxlan_destroy(const char *name)
+{
+    return netdev_linux_destroy(name);
+}
+
+static int
+netdev_gre_destroy(const char *name)
+{
+    return netdev_linux_destroy(name);
+}
+
+static int
+netdev_geneve_destroy(const char *name)
+{
+    return netdev_linux_destroy(name);
+}
+
+/*
+ * On some older systems, these enums are not defined.
+ */
+
+#ifndef IFLA_VXLAN_MAX
+#define IFLA_VXLAN_MAX 0
+#define IFLA_VXLAN_PORT 15
+#endif
+#if IFLA_VXLAN_MAX < 20
+#define IFLA_VXLAN_UDP_ZERO_CSUM6_RX 20
+#define IFLA_VXLAN_GBP 23
+#define IFLA_VXLAN_COLLECT_METADATA 25
+#endif
+
+#if IFLA_GRE_MAX < 18
+#define IFLA_GRE_COLLECT_METADATA 18
+#endif
+
+#ifndef IFLA_GENEVE_MAX
+#define IFLA_GENEVE_MAX 0
+#define IFLA_GENEVE_PORT 5
+#endif
+
+#if IFLA_GENEVE_MAX < 6
+#define IFLA_GENEVE_COLLECT_METADATA 6
+#endif
+#if IFLA_GENEVE_MAX < 10
+#define IFLA_GENEVE_UDP_ZERO_CSUM6_RX 10
+#endif
+
+static int
+netdev_vxlan_create(struct netdev *netdev)
+{
+    int err;
+    struct ofpbuf request, *reply;
+    size_t linkinfo_off, infodata_off;
+    char namebuf[NETDEV_VPORT_NAME_BUFSIZE];
+    const char *name = netdev_vport_get_dpif_port(netdev,
+                                                  namebuf, sizeof namebuf);
+    struct ifinfomsg *ifinfo;
+    const struct netdev_tunnel_config *tnl_cfg;
+    tnl_cfg = netdev_get_tunnel_config(netdev);
+    if (!tnl_cfg) { /* or assert? */
+        return EINVAL;
+    }
+
+    ofpbuf_init(&request, 0);
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
+    nl_msg_put_string(&request, IFLA_IFNAME, name);
+    nl_msg_put_u32(&request, IFLA_MTU, UINT16_MAX);
+    linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
+        nl_msg_put_string(&request, IFLA_INFO_KIND, "vxlan");
+        infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
+            nl_msg_put_u8(&request, IFLA_VXLAN_LEARNING, 0);
+            nl_msg_put_u8(&request, IFLA_VXLAN_COLLECT_METADATA, 1);
+            nl_msg_put_u8(&request, IFLA_VXLAN_UDP_ZERO_CSUM6_RX, 1);
+            if (tnl_cfg->exts & (1 << OVS_VXLAN_EXT_GBP)) {
+                nl_msg_put_flag(&request, IFLA_VXLAN_GBP);
+            }
+            nl_msg_put_be16(&request, IFLA_VXLAN_PORT, tnl_cfg->dst_port);
+        nl_msg_end_nested(&request, infodata_off);
+    nl_msg_end_nested(&request, linkinfo_off);
+
+    err = nl_transact(NETLINK_ROUTE, &request, &reply);
+
+    if (!err) {
+        ofpbuf_uninit(reply);
+    }
+
+    /*
+     * Linux versions older than 4.3 will return EINVAL in case the VID is not
+     * set, which is sufficient to verify COLLECT_METADATA is supported.
+     */
+    if (err == EINVAL) {
+        err = EOPNOTSUPP;
+    }
+
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+/*
+ * On some Linux versions, creating the device with IFLA_GRE_COLLECT_METADATA
+ * will succeed, even though that attribute is not supported. We need to verify
+ * the device has been created with that attribute. In case it has not, we
+ * destroy it and use the compat code.
+ */
+static int
+netdev_gre_verify(const char *name)
+{
+    int err;
+    struct ofpbuf request, *reply;
+    struct ifinfomsg *ifmsg;
+
+    static const struct nl_policy rtlink_policy[] = {
+        [IFLA_LINKINFO] = { .type = NL_A_NESTED },
+    };
+    static const struct nl_policy linkinfo_policy[] = {
+        [IFLA_INFO_KIND] = { .type = NL_A_STRING },
+        [IFLA_INFO_DATA] = { .type = NL_A_NESTED },
+    };
+    static const struct nl_policy gre_policy[] = {
+        [IFLA_GRE_COLLECT_METADATA] = { .type = NL_A_FLAG },
+    };
+
+    ofpbuf_init(&request, 0);
+    nl_msg_put_nlmsghdr(&request, 0, RTM_GETLINK,
+                        NLM_F_REQUEST);
+    ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    nl_msg_put_string(&request, IFLA_IFNAME, name);
+
+    err = nl_transact(NETLINK_ROUTE, &request, &reply);
+    if (!err) {
+        struct nlattr *rtlink[ARRAY_SIZE(rtlink_policy)];
+        struct nlattr *linkinfo[ARRAY_SIZE(linkinfo_policy)];
+        struct nlattr *gre[ARRAY_SIZE(gre_policy)];
+
+        err = EINVAL;
+        ifmsg = ofpbuf_at(reply, NLMSG_HDRLEN, sizeof *ifmsg);
+        if (nl_policy_parse(reply, NLMSG_HDRLEN + sizeof *ifmsg,
+            rtlink_policy, rtlink, ARRAY_SIZE(rtlink_policy))) {
+            if (nl_parse_nested(rtlink[IFLA_LINKINFO], linkinfo_policy,
+                linkinfo, ARRAY_SIZE(linkinfo_policy)) &&
+                !strcmp(nl_attr_get_string(linkinfo[IFLA_INFO_KIND]),
+                        "gretap")) {
+                if (nl_parse_nested(linkinfo[IFLA_INFO_DATA], gre_policy, gre,
+                    ARRAY_SIZE(gre_policy)) &&
+                    nl_attr_get_flag(gre[IFLA_GRE_COLLECT_METADATA])) {
+                        err = 0;
+                }
+            }
         }
+        ofpbuf_uninit(reply);
     }
-    free(socksp);
+    ofpbuf_uninit(&request);
+    return err;
+}
 
-exit:
-    ofpbuf_delete(buf);
-    free(upcall_pids);
+static int
+netdev_gre_create(struct netdev *netdev)
+{
+    int err;
+    struct ofpbuf request, *reply;
+    size_t linkinfo_off, infodata_off;
+    char namebuf[NETDEV_VPORT_NAME_BUFSIZE];
+    const char *name = netdev_vport_get_dpif_port(netdev,
+                                                  namebuf, sizeof namebuf);
+    struct ifinfomsg *ifinfo;
+    const struct netdev_tunnel_config *tnl_cfg;
+    tnl_cfg = netdev_get_tunnel_config(netdev);
+    if (!tnl_cfg) { /* or assert? */
+        return EINVAL;
+    }
+
+    ofpbuf_init(&request, 0);
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
+    nl_msg_put_string(&request, IFLA_IFNAME, name);
+    nl_msg_put_u32(&request, IFLA_MTU, UINT16_MAX);
+    linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
+        nl_msg_put_string(&request, IFLA_INFO_KIND, "gretap");
+        infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
+            nl_msg_put_flag(&request, IFLA_GRE_COLLECT_METADATA);
+        nl_msg_end_nested(&request, infodata_off);
+    nl_msg_end_nested(&request, linkinfo_off);
+
+    err = nl_transact(NETLINK_ROUTE, &request, &reply);
 
+    if (!err) {
+        ofpbuf_uninit(reply);
+    }
+
+    if (!err && (err = netdev_gre_verify(name))) {
+        netdev_gre_destroy(name);
+    }
+
+    /*
+     * If tunnel metadata is not supported, EEXIST will be returned for zero
+     * addresses tunnel. We still need to verify metadata has been set as above.
+     */
+    if (err == EINVAL || err == EEXIST) {
+        err = EOPNOTSUPP;
+    }
+
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+static int
+netdev_geneve_create(struct netdev *netdev)
+{
+    int err;
+    struct ofpbuf request, *reply;
+    size_t linkinfo_off, infodata_off;
+    char namebuf[NETDEV_VPORT_NAME_BUFSIZE];
+    const char *name = netdev_vport_get_dpif_port(netdev,
+                                                  namebuf, sizeof namebuf);
+    struct ifinfomsg *ifinfo;
+    const struct netdev_tunnel_config *tnl_cfg;
+    tnl_cfg = netdev_get_tunnel_config(netdev);
+    if (!tnl_cfg) { /* or assert? */
+        return EINVAL;
+    }
+
+    ofpbuf_init(&request, 0);
+    nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
+                        NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE);
+    ifinfo = ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
+    ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
+    nl_msg_put_string(&request, IFLA_IFNAME, name);
+    nl_msg_put_u32(&request, IFLA_MTU, UINT16_MAX);
+    linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
+        nl_msg_put_string(&request, IFLA_INFO_KIND, "geneve");
+        infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
+            nl_msg_put_flag(&request, IFLA_GENEVE_COLLECT_METADATA);
+            nl_msg_put_u8(&request, IFLA_GENEVE_UDP_ZERO_CSUM6_RX, 1);
+            nl_msg_put_be16(&request, IFLA_GENEVE_PORT, tnl_cfg->dst_port);
+        nl_msg_end_nested(&request, infodata_off);
+    nl_msg_end_nested(&request, linkinfo_off);
+
+    err = nl_transact(NETLINK_ROUTE, &request, &reply);
+
+    if (!err) {
+        ofpbuf_uninit(reply);
+    }
+
+    /*
+     * Linux versions older than 4.3 will return EINVAL in case the GENEVE_ID is
+     * not set, which is sufficient to verify COLLECT_METADATA is supported.
+     */
+    if (err == EINVAL) {
+        err = EOPNOTSUPP;
+    }
+
+    ofpbuf_uninit(&request);
+    return err;
+}
+
+#else
+
+static int
+netdev_vxlan_create(struct netdev *netdev OVS_UNUSED)
+{
+    return EOPNOTSUPP;
+}
+
+static int
+netdev_gre_create(struct netdev *netdev OVS_UNUSED)
+{
+    return EOPNOTSUPP;
+}
+
+static int
+netdev_geneve_create(struct netdev *netdev OVS_UNUSED)
+{
+    return EOPNOTSUPP;
+}
+
+static int
+netdev_vxlan_destroy(const char *name OVS_UNUSED)
+{
+    return EOPNOTSUPP;
+}
+
+static int
+netdev_gre_destroy(const char *name OVS_UNUSED)
+{
+    return EOPNOTSUPP;
+}
+
+static int
+netdev_geneve_destroy(const char *name OVS_UNUSED)
+{
+    return EOPNOTSUPP;
+}
+
+#endif
+
+static int
+dpif_netlink_port_query__(const struct dpif_netlink *dpif, odp_port_t port_no,
+                          const char *port_name, struct dpif_port *dpif_port);
+
+static int
+dpif_netlink_port_create(struct netdev *netdev)
+{
+    switch (netdev_to_ovs_vport_type(netdev_get_type(netdev))) {
+    case OVS_VPORT_TYPE_VXLAN:
+        return netdev_vxlan_create(netdev);
+    case OVS_VPORT_TYPE_GRE:
+        return netdev_gre_create(netdev);
+    case OVS_VPORT_TYPE_GENEVE:
+        return netdev_geneve_create(netdev);
+    case OVS_VPORT_TYPE_NETDEV:
+    case OVS_VPORT_TYPE_INTERNAL:
+    case OVS_VPORT_TYPE_LISP:
+    case OVS_VPORT_TYPE_STT:
+    case OVS_VPORT_TYPE_UNSPEC:
+    case __OVS_VPORT_TYPE_MAX:
+    default:
+        return EOPNOTSUPP;
+    }
+    return 0;
+}
+
+static int
+dpif_netlink_port_destroy(const char *name, const char *type)
+{
+    switch (netdev_to_ovs_vport_type(type)) {
+    case OVS_VPORT_TYPE_VXLAN:
+        return netdev_vxlan_destroy(name);
+    case OVS_VPORT_TYPE_GRE:
+        return netdev_gre_destroy(name);
+    case OVS_VPORT_TYPE_GENEVE:
+        return netdev_geneve_destroy(name);
+    case OVS_VPORT_TYPE_NETDEV:
+    case OVS_VPORT_TYPE_INTERNAL:
+    case OVS_VPORT_TYPE_LISP:
+    case OVS_VPORT_TYPE_STT:
+    case OVS_VPORT_TYPE_UNSPEC:
+    case __OVS_VPORT_TYPE_MAX:
+    default:
+        return EOPNOTSUPP;
+    }
+    return 0;
+}
+
+static int
+dpif_netlink_port_create_and_add(struct dpif_netlink *dpif, struct netdev *netdev,
+                           odp_port_t *port_nop)
+    OVS_REQ_WRLOCK(dpif->upcall_lock)
+{
+    int error;
+    char namebuf[NETDEV_VPORT_NAME_BUFSIZE];
+    const char *name = netdev_vport_get_dpif_port(netdev,
+                                                  namebuf, sizeof namebuf);
+
+    error = dpif_netlink_port_create(netdev);
+    if (error) {
+        return error;
+    }
+
+    error = dpif_netlink_port_add__(dpif, name, OVS_VPORT_TYPE_NETDEV, NULL, port_nop);
+    if (error) {
+        VLOG_DBG("failed to add port, destroying: %d", error);
+        dpif_netlink_port_destroy(name, netdev_get_type(netdev));
+    }
     return error;
 }
 
@@ -923,7 +1338,10 @@ dpif_netlink_port_add(struct dpif *dpif_, struct netdev *netdev,
     int error;
 
     fat_rwlock_wrlock(&dpif->upcall_lock);
-    error = dpif_netlink_port_add__(dpif, netdev, port_nop);
+    error = dpif_netlink_port_create_and_add(dpif, netdev, port_nop);
+    if (error == EOPNOTSUPP) {
+        error = dpif_netlink_port_add_compat(dpif, netdev, port_nop);
+    }
     fat_rwlock_unlock(&dpif->upcall_lock);
 
     return error;
@@ -935,6 +1353,12 @@ dpif_netlink_port_del__(struct dpif_netlink *dpif, odp_port_t port_no)
 {
     struct dpif_netlink_vport vport;
     int error;
+    struct dpif_port dpif_port;
+
+    error = dpif_netlink_port_query__(dpif, port_no, NULL, &dpif_port);
+    if (error) {
+        return error;
+    }
 
     dpif_netlink_vport_init(&vport);
     vport.cmd = OVS_VPORT_CMD_DEL;
@@ -944,6 +1368,9 @@ dpif_netlink_port_del__(struct dpif_netlink *dpif, odp_port_t port_no)
 
     vport_del_channels(dpif, port_no);
 
+    dpif_netlink_port_destroy(dpif_port.name, dpif_port.type);
+    dpif_port_destroy(&dpif_port);
+
     return error;
 }
 
@@ -2400,9 +2827,9 @@ dpif_netlink_init(void)
         error = nl_lookup_genl_family(OVS_DATAPATH_FAMILY,
                                       &ovs_datapath_family);
         if (error) {
-            VLOG_ERR("Generic Netlink family '%s' does not exist. "
-                     "The Open vSwitch kernel module is probably not loaded.",
-                     OVS_DATAPATH_FAMILY);
+            VLOG_WARN("Generic Netlink family '%s' does not exist. "
+                      "The Open vSwitch kernel module is probably not loaded.",
+                      OVS_DATAPATH_FAMILY);
         }
         if (!error) {
             error = nl_lookup_genl_family(OVS_VPORT_FAMILY, &ovs_vport_family);