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