Add support for a new option in local OvS database
"external_ids:ovn-evpn-vxlan-ports" which is comma separated list
of VXLAN destination ports. Create VXLAN tunnel for each port
in the list which can then be used by OVN for EVPN traffic.

Acked-by: Xavier Simonart <xsimo...@redhat.com>
Signed-off-by: Ales Musil <amu...@redhat.com>
---
v4: Rebase on top of latest main.
    Add Xavier's ack.
    Address review comments:
    - Fix wording in NEWS.

v3: Newly added commit.
---
 NEWS                            |   3 +
 controller/chassis.c            |  19 ++++++
 controller/encaps.c             | 115 +++++++++++++++++++++++++++++++-
 controller/ovn-controller.8.xml |  20 ++++++
 lib/ovn-util.c                  |  14 ++++
 lib/ovn-util.h                  |   1 +
 tests/ovn-controller.at         |  57 ++++++++++++++++
 7 files changed, 228 insertions(+), 1 deletion(-)

diff --git a/NEWS b/NEWS
index 54d676be8..7acade389 100644
--- a/NEWS
+++ b/NEWS
@@ -46,6 +46,9 @@ Post v25.03.0
        Routers and Logical Router Ports which refines the way in which
        chassis-specific Advertised_Routes (e.g., for NAT and LB IPs) are
        advertised.
+     * Add the option "external-ids:ovn-evpn-vxlan-ports" for the
+       Open_vSwitch database. This option allows CMS to set list of UDP
+       destination ports that will be used to create the VXLAN tunnels.
 
 OVN v25.03.0 - 07 Mar 2025
 --------------------------
diff --git a/controller/chassis.c b/controller/chassis.c
index 7616069b7..34adc98be 100644
--- a/controller/chassis.c
+++ b/controller/chassis.c
@@ -58,6 +58,7 @@ struct ovs_chassis_cfg {
     const char *trim_limit_lflow_cache;
     const char *trim_wmark_perc_lflow_cache;
     const char *trim_timeout_ms;
+    const char *evpn_vxlan_port;
 
     /* Set of encap types parsed from the 'ovn-encap-type' external-id. */
     struct sset encap_type_set;
@@ -198,6 +199,13 @@ get_encap_csum(const struct smap *ext_ids, const char 
*chassis_id)
                                          "ovn-encap-csum", "true");
 }
 
+static const char *
+get_evpn_vxlan_port(const struct smap *ext_ids, const char *chassis_id)
+{
+    return get_chassis_external_id_value(ext_ids, chassis_id,
+                                         "ovn-evpn-vxlan-ports", "");
+}
+
 static const char *
 get_datapath_type(const struct ovsrec_bridge *br_int)
 {
@@ -332,6 +340,8 @@ chassis_parse_ovs_config(const struct 
ovsrec_open_vswitch_table *ovs_table,
         get_trim_wmark_perc_lflow_cache(&cfg->external_ids, chassis_id);
     ovs_cfg->trim_timeout_ms =
         get_trim_timeout(&cfg->external_ids, chassis_id);
+    ovs_cfg->evpn_vxlan_port =
+        get_evpn_vxlan_port(&cfg->external_ids, chassis_id);
 
     chassis_parse_ovs_encap_type(encap_type, &ovs_cfg->encap_type_set);
 
@@ -384,6 +394,7 @@ chassis_build_other_config(const struct ovs_chassis_cfg 
*ovs_cfg,
     smap_replace(config, "ovn-trim-timeout-ms", ovs_cfg->trim_timeout_ms);
     smap_replace(config, "iface-types", ds_cstr_ro(&ovs_cfg->iface_types));
     smap_replace(config, "ovn-chassis-mac-mappings", ovs_cfg->chassis_macs);
+    smap_replace(config, "ovn-evpn-vxlan-ports", ovs_cfg->evpn_vxlan_port);
     smap_replace(config, "is-interconn",
                  ovs_cfg->is_interconn ? "true" : "false");
     smap_replace(config, OVN_FEATURE_PORT_UP_NOTIF, "true");
@@ -504,6 +515,13 @@ chassis_other_config_changed(const struct ovs_chassis_cfg 
*ovs_cfg,
         return true;
     }
 
+    const char *chassis_evpn_vxlan_port =
+        get_evpn_vxlan_port(&chassis_rec->other_config, chassis_rec->name);
+    if (strcmp(ovs_cfg->evpn_vxlan_port, chassis_evpn_vxlan_port)) {
+        return true;
+    }
+
+
     if (!smap_get_bool(&chassis_rec->other_config, OVN_FEATURE_PORT_UP_NOTIF,
                        false)) {
         return true;
@@ -722,6 +740,7 @@ update_supported_sset(struct sset *supported)
     sset_add(supported, "iface-types");
     sset_add(supported, "ovn-chassis-mac-mappings");
     sset_add(supported, "is-interconn");
+    sset_add(supported, "ovn-evpn-vxlan-ports");
 
     /* Internal options. */
     sset_add(supported, "is-vtep");
diff --git a/controller/encaps.c b/controller/encaps.c
index b5ef66371..67f8c9c9c 100644
--- a/controller/encaps.c
+++ b/controller/encaps.c
@@ -64,6 +64,9 @@ struct tunnel_ctx {
      * adding a new tunnel. */
     struct sset port_names;
 
+    /* Contains 'struct ovsrec_port' by name if it's evpn tunnel. */
+    struct shash evpn_tunnels;
+
     struct ovsdb_idl_txn *ovs_txn;
     const struct ovsrec_open_vswitch_table *ovs_table;
     const struct ovsrec_bridge *br_int;
@@ -442,7 +445,10 @@ clear_old_tunnels(const struct ovsrec_bridge *old_br_int, 
const char *prefix,
     for (size_t i = 0; i < old_br_int->n_ports; i++) {
         const struct ovsrec_port *port = old_br_int->ports[i];
         const char *id = smap_get(&port->external_ids, OVN_TUNNEL_ID);
-        if (id && !strncmp(port->name, prefix, prefix_len)) {
+        const bool evpn_tunnel =
+            smap_get_bool(&port->external_ids, "ovn-evpn-tunnel", false);
+        if (!strncmp(port->name, prefix, prefix_len) &&
+            (id || evpn_tunnel)) {
             VLOG_DBG("Clearing old tunnel port \"%s\" (%s) from bridge "
                      "\"%s\".", port->name, id, old_br_int->name);
             ovsrec_bridge_update_ports_delvalue(old_br_int, port);
@@ -450,6 +456,97 @@ clear_old_tunnels(const struct ovsrec_bridge *old_br_int, 
const char *prefix,
     }
 }
 
+static bool
+is_evpn_tunnel_port(const struct ovsrec_port *port, const char *dst_port)
+{
+    if (!smap_get_bool(&port->external_ids, "ovn-evpn-tunnel", false)) {
+        return false;
+    }
+
+    if (port->n_interfaces != 1) {
+        return false;
+    }
+
+    const struct ovsrec_interface *iface = port->interfaces[0];
+    if (strcmp(iface->type, "vxlan")) {
+        return false;
+    }
+
+    if (strcmp(smap_get_def(&iface->options, "local_ip", ""), "flow") ||
+        strcmp(smap_get_def(&iface->options, "remote_ip", ""), "flow") ||
+        strcmp(smap_get_def(&iface->options, "key", ""), "flow") ||
+        strcmp(smap_get_def(&iface->options, "dst_port", ""), dst_port)) {
+        return false;
+    }
+
+    return true;
+}
+
+static void
+create_evpn_tunnels(struct tunnel_ctx *tc)
+{
+    const char *evpn_vxlan_ports =
+        smap_get(&tc->this_chassis->other_config, "ovn-evpn-vxlan-ports");
+    if (!evpn_vxlan_ports) {
+        return;
+    }
+
+    /* Create smap of common tunnel options. */
+    struct smap options = SMAP_INITIALIZER(&options);
+    smap_add(&options, "local_ip", "flow");
+    smap_add(&options, "remote_ip", "flow");
+    smap_add(&options, "key", "flow");
+
+    struct sset vxlan_ports;
+    sset_from_delimited_string(&vxlan_ports, evpn_vxlan_ports, ",");
+    const char *idx = get_chassis_idx(tc->ovs_table);
+
+    const char *vxlan_port;
+    SSET_FOR_EACH (vxlan_port, &vxlan_ports) {
+        unsigned short us;
+        if (!ovn_str_to_ushort(vxlan_port, 10, &us) || !us) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "Invalid VXLAN port number '%s'", vxlan_port);
+            continue;
+        }
+
+        char *name = xasprintf("ovn%s-evpn-%s", idx, vxlan_port);
+        const struct ovsrec_port *port =
+            shash_find_and_delete(&tc->evpn_tunnels, name);
+
+        if (!port) {
+            port = ovsrec_port_insert(tc->ovs_txn);
+            ovsrec_port_set_name(port, name);
+
+            const struct smap id = SMAP_CONST1(&id, "ovn-evpn-tunnel", "true");
+            ovsrec_port_set_external_ids(port, &id);
+
+            ovsrec_bridge_update_ports_addvalue(tc->br_int, port);
+        }
+
+        if (!is_evpn_tunnel_port(port, vxlan_port)) {
+            struct ovsrec_interface *iface =
+                ovsrec_interface_insert(tc->ovs_txn);
+            ovsrec_interface_set_name(iface, name);
+            ovsrec_interface_set_type(iface, "vxlan");
+
+            smap_replace(&options, "dst_port", vxlan_port);
+            ovsrec_interface_set_options(iface, &options);
+
+            const struct smap id = SMAP_CONST1(&id, "ovn-evpn-tunnel", "true");
+            ovsrec_port_set_external_ids(port, &id);
+
+            ovsrec_port_set_interfaces(port, &iface, 1);
+        }
+
+        free(name);
+    }
+
+    smap_destroy(&options);
+    sset_destroy(&vxlan_ports);
+}
+
+
 void
 encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
            const struct ovsrec_bridge *br_int,
@@ -498,6 +595,7 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
     struct tunnel_ctx tc = {
         .tunnel = SHASH_INITIALIZER(&tc.tunnel),
         .port_names = SSET_INITIALIZER(&tc.port_names),
+        .evpn_tunnels = SHASH_INITIALIZER(&tc.evpn_tunnels),
         .br_int = br_int,
         .this_chassis = this_chassis,
         .ovs_table = ovs_table,
@@ -528,6 +626,10 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
                 ovsrec_bridge_update_ports_delvalue(br_int, port);
             }
         }
+
+        if (smap_get_bool(&port->external_ids, "ovn-evpn-tunnel", false)) {
+            shash_add(&tc.evpn_tunnels, port->name, port);
+        }
     }
 
     SBREC_CHASSIS_TABLE_FOR_EACH (chassis_rec, chassis_table) {
@@ -558,6 +660,8 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
         }
     }
 
+    create_evpn_tunnels(&tc);
+
     /* Delete any existing OVN tunnels that were not still around. */
     struct shash_node *node;
     SHASH_FOR_EACH_SAFE (node, &tc.tunnel) {
@@ -566,8 +670,17 @@ encaps_run(struct ovsdb_idl_txn *ovs_idl_txn,
         shash_delete(&tc.tunnel, node);
         free(tunnel);
     }
+
+    /* Delete any stale EVPN tunnels. */
+    SHASH_FOR_EACH_SAFE (node, &tc.evpn_tunnels) {
+        const struct ovsrec_port *port = node->data;
+        ovsrec_bridge_update_ports_delvalue(br_int, port);
+        shash_delete(&tc.evpn_tunnels, node);
+    }
+
     shash_destroy(&tc.tunnel);
     sset_destroy(&tc.port_names);
+    shash_destroy(&tc.evpn_tunnels);
 }
 
 /* Returns true if the database is all cleaned up, false if more work is
diff --git a/controller/ovn-controller.8.xml b/controller/ovn-controller.8.xml
index 3f2cf5bae..e602ebca8 100644
--- a/controller/ovn-controller.8.xml
+++ b/controller/ovn-controller.8.xml
@@ -392,6 +392,13 @@
         exit. In order to keep backward compatibility the
         <code>--restart</code> exit flag has priority over this flag.
       </dd>
+
+      <dt><code>external_ids:ovn-evpn-vxlan-ports</code></dt>
+      <dd>
+        Comma separated list of UDP ports used as a destination port for
+        the EVPN tunnels created by this ovn-controller. NOTE: this feature is
+        experimental and may be subject to removal/change in the future.
+      </dd>
     </dl>
 
     <p>
@@ -626,6 +633,19 @@
           <code>external_ids:ovn-installed-ts</code>.
         </p>
       </dd>
+
+      <dt>
+        <code>external_ids:ovn-evpn-tunnel</code> in the
+        <code>Port</code> and <code>Interface</code> table
+      </dt>
+
+      <dd>
+        <p>
+          Presence of this key indicates that the
+          <code>Port/Interface</code> was created by ovn-controller to
+          be used as tunnel port for EVPN traffic.
+        </p>
+      </dd>
     </dl>
 
     <h1>OVN Southbound Database Usage</h1>
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index 5df1403bc..2d4ebbb0a 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -865,6 +865,20 @@ ovn_smap_get_llong(const struct smap *smap, const char 
*key, long long def)
     return ll_value;
 }
 
+bool
+ovn_str_to_ushort(const char *s, int base, unsigned short *u)
+{
+    long long ll;
+    bool ok = str_to_llong(s, base, &ll);
+    if (!ok || ll < 0 || ll > USHRT_MAX) {
+        *u = 0;
+        return false;
+    } else {
+        *u = ll;
+        return true;
+    }
+}
+
 /* For a 'key' of the form "IP:port" or just "IP", sets 'port',
  * 'ip_address' and 'ip' ('struct in6_addr' IPv6 or IPv4 mapped address).
  * The caller must free() the memory allocated for 'ip_address'.
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index 6bd46f8a5..427defc5b 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -221,6 +221,7 @@ char *str_tolower(const char *orig);
 
 long long ovn_smap_get_llong(const struct smap *smap, const char *key,
                              long long def);
+bool ovn_str_to_ushort(const char *, int base, unsigned short *);
 
 /* OVN daemon options. Taken from ovs/lib/daemon.h. */
 #define OVN_DAEMON_OPTION_ENUMS                     \
diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
index 95fbcc0be..c5d8cb5c1 100644
--- a/tests/ovn-controller.at
+++ b/tests/ovn-controller.at
@@ -3803,3 +3803,60 @@ check ovn-nbctl --wait=hv sync
 
 OVN_CLEANUP([hv1])
 AT_CLEANUP
+
+AT_SETUP([ovn-controller - EVPN tunnel])
+ovn_start
+
+net_add n1
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.11 24 geneve,vxlan
+check ovn-nbctl --wait=hv sync
+
+wait_ports() {
+    local n_ports=$1
+
+    OVS_WAIT_UNTIL([
+        test "$(ovs-vsctl --format=table --no-headings find port 
external-ids:ovn-evpn-tunnel="true" | wc -l)" = "$n_ports"
+    ])
+}
+
+check_tunnel_port() {
+    local port=$1
+
+    local tunnel_id=$(ovs-vsctl --bare --columns _uuid find port 
name="ovn-evpn-$port")
+    AT_CHECK([ovs-vsctl --bare --columns ports find bridge name="br-int" | 
grep -q "$tunnel_id"])
+}
+
+check ovs-vsctl set Open_vSwitch . external-ids:ovn-evpn-vxlan-ports="4789"
+wait_ports 1
+check_tunnel_port 4789
+
+check ovs-vsctl set Open_vSwitch . 
external-ids:ovn-evpn-vxlan-ports="4789,4790"
+wait_ports 2
+check_tunnel_port 4789
+check_tunnel_port 4790
+
+# Stop ovn-controller
+OVN_CONTROLLER_EXIT([hv1],[])
+start_daemon ovn-controller
+check ovn-nbctl --wait=hv sync
+
+wait_ports 2
+check_tunnel_port 4789
+check_tunnel_port 4790
+
+check ovs-vsctl set Open_vSwitch . external-ids:ovn-evpn-vxlan-ports="4790"
+wait_ports 1
+check_tunnel_port 4790
+
+ovs-vsctl remove Open_vSwitch . external-ids ovn-evpn-vxlan-ports
+wait_ports 0
+
+check ovs-vsctl set Open_vSwitch . external-ids:ovn-evpn-vxlan-ports="wrong"
+wait_ports 0
+
+OVN_CLEANUP([hv1
+/Invalid VXLAN port number.*/d])
+AT_CLEANUP
-- 
2.50.1

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to