On 8/19/26 3:40 PM, Rukomoinikova Aleksandra wrote:
> On 19.08.2026 14:34, Dumitru Ceara wrote:
>> On 7/20/26 6:59 PM, Alexandra Rukomoinikova via dev wrote:
>>> When a Logical Switch has dynamic-routing-vni and
>>> dynamic-routing-maintain-evpn=mvd set, ovn-controller automatically
>>> creates and manages the Linux kernel interfaces required for
>>> EVPN/BGP: a VRF (ovnvrf<vni>), a bridge (ovnbr<vni>), a VXLAN
>>> interface (ovnvxlan<vni>) and, when FDB redistribution is enabled,
>>> a dummy interface (ovnlo<vni>) used to advertise MACs.
>>>
>>> For L3 VNI support (IP redistribution), dynamic-routing-maintain-evpn-lrp
>>> must name a Logical Router Port; its MAC is applied to the bridge
>>> interface so BGP can use it as next-hop. Interfaces are recreated
>>> whenever a change (local IP, VXLAN port, has_addr or redistribute
>>> mode) can't be applied in place, and are torn down on shutdown or
>>> when the VNI is no longer configured.
>>>
>>> The low-level netlink calls to create/delete devices and set their
>>> master/MAC, previously duplicated between route-exchange and
>>> neighbor-exchange, are consolidated into a new
>>> controller/netlink-utils.c. evpn_local_ip_map also moves from
>>> controller/physical.c to lib/ovn-util.c so it can be shared with the
>>> new code.
>>>
>>> Add system-ovn.at coverage and document the new options in
>>> ovn-nb.xml
>>>
>>> Signed-off-by: Alexandra Rukomoinikova <[email protected]>
>>> ---
>> Hi Alexandra,
>>
>> Thanks for the new feature patch! I didn't get a chance to review it
>> yet and unfortunately it will have to wait for 27.03 as it didn't get
>> any reviews before soft freeze.
>
> HI!
>
Hi Alexandra,
> I think it's great that you didn't get around to reviewing this yet;
> I’ve reworked the patch a bit on my end, found some issues, and will
> send a new version.
>
OK.
>> On the other hand that gives us more time to think about the user facing
>> configuration.
>>
>> Some thoughts:
>> - dynamic-routing-maintain-evpn-lrp=LRP is actually used in
>> ovn-controller where we can only access SB data so Port_Bindings. But,
>> at least theoretically, those can have multiple mac addresses. Which
>> one should ovn-controller choose? Is it maybe better to make the MAC
>> address a different EVPN LS configuration?
> Right now the logic picks the first MAC address, and yes, that does look
> odd. But honestly, I like the idea of options for mac rather for router
> port name even less. First, I consider the northbound database the
> source of truth: if the port already has a MAC address, why would I have
> to specify it a second time? Tying it to the port also seems more robust
> — if I change the address there in NB, the controller picks it up on its
> own, whereas with an option the CMS has to go and update it separately
> as well. And "take the first MAC by default" seems to cover almost every
> case. That said, I'm not sure where the right compromise is here — if
> you think an explicit MAC option is better, that's fine, I can do that.
I think they're both equally ugly but the
dynamic-routing-maintain-evpn-lrp=LRP option is ambiguous.
I guess we can discuss some more on the new version of the patch, while
looking at the code.
>>
>> - what about SVD (single vxlan device) mode? Can we find a reasonable
>> way of having ovn autoconfigure that too so we don't have this new
>> feature that only covers half of the deployment modes? What would be
>> needed? I guess we'd need VNI <-> VLAN mappings passed somehow.
>> Anything else?
>
> It seems like that's all, and yes, I can finish the SVD part, I don't
> see any problems with it.
>
> I have two questions.
>
> 1. Right now, anywhere in the controller code, any netlink error
> triggers a recompute. That approach feels concerning to me: a
> netlink error doesn't invalidate any engine input — the desired
> state hasn't changed, so the recompute derives the same result and
> re-issues the same calls. It's really a retry expressed as
> invalidation. What do you think, is this worth doing something
> about? What bothers me is how this looks in production: if the error
> somehow isn't caused by OVN's own code but by an external factor, we
> end up with a controller spinning at 100% CPU until someone fixes it
> by hand.
That is a very good point. We should fix that but the question is: how
do you differentiate between transient errors and other types of errors?
For the transient ones we should probably try to recompute (maybe with
a backoff).
> 2. In the current patch, the VNI for the fake vxlan port is derived as
> an offset: if the user specifies, say, 67, what's actually used is
> 60000 + 67. This limits the range of usable VNIs. What do you think
> — leave it as is, or add a separate option after all? Though I don't
> much like the idea of yet another option here either. or create an
> option with some pool of free ports?
>
This makes it so visible that we're relying on a (kind of) broken FRR
behavior, i.e., the fact that FRR ignores the vxlan interface configured
local port. I vaguely remembered a discussion along the same lines
related to our system tests and VNI choice. I don't think there's any
good way of doing this.
Probably the only reasonable way is to make FRR work with OVS vxlan
tunnels directly or make OVN capable of monitoring route/interface
tables from other network namespaces than the one it runs into.
Everything else will be less than ideal so I guess I'd just document how
it's derived as an offset for now.
Regards,
Dumitru
>>
>> Regards,
>> Dumitru
>>
>>
>>> v1 --> v2: minor cleanups, no logic changed, rebased
>>> ---
>>> NEWS | 7 +
>>> controller/automake.mk | 2 +
>>> controller/host-if-monitor.c | 15 ++
>>> controller/host-if-monitor.h | 2 +-
>>> controller/neighbor-exchange-netlink.c | 103 +++++++-
>>> controller/neighbor-exchange-netlink.h | 11 +
>>> controller/neighbor-exchange-stub.c | 10 +
>>> controller/neighbor-exchange.c | 324 +++++++++++++++++++++++++
>>> controller/neighbor-exchange.h | 8 +
>>> controller/neighbor.c | 182 +++++++++++---
>>> controller/neighbor.h | 34 +++
>>> controller/netlink-utils.c | 148 +++++++++++
>>> controller/netlink-utils.h | 41 ++++
>>> controller/ovn-controller.c | 20 ++
>>> controller/physical.c | 157 ------------
>>> controller/route-exchange-netlink.c | 45 +---
>>> controller/route-exchange-netlink.h | 5 -
>>> controller/route-exchange.c | 1 +
>>> lib/ovn-util.c | 157 ++++++++++++
>>> lib/ovn-util.h | 25 ++
>>> northd/en-datapath-logical-switch.c | 14 ++
>>> ovn-nb.xml | 88 ++++++-
>>> tests/automake.mk | 2 +
>>> tests/system-ovn.at | 186 ++++++++++++++
>>> 24 files changed, 1337 insertions(+), 250 deletions(-)
>>> create mode 100644 controller/netlink-utils.c
>>> create mode 100644 controller/netlink-utils.h
>>>
>>> diff --git a/NEWS b/NEWS
>>> index 82bf1a272..43c3e5af1 100644
>>> --- a/NEWS
>>> +++ b/NEWS
>>> @@ -74,6 +74,13 @@ Post v26.03.0
>>> (type 11) and Parameter Problem (type 12) - generated by an external
>>> router are un-NATed correctly. This makes Path MTU discovery and
>>> traceroute work through stateless NAT.
>>> + - Add other_config "dynamic-routing-maintain-evpn" option for Logical
>>> + Switche. When set to "mvd", ovn-controller creates and maintains
>>> + Linux kernel interfaces required for EVPN/VXLAN itself: VRF, bridge,
>>> + VXLAN device per VNI, for FDB redistribution, a dummy interface.
>>> + Other_config "dynamic-routing-maintain-evpn-lrp" option names a
>>> Logical
>>> + Router Port whose MAC applied to the bridge interface for router port
>>> + MAC-IP redistribution support.
>>>
>>> OVN v26.03.0 - xxx xx xxxx
>>> --------------------------
>>> diff --git a/controller/automake.mk b/controller/automake.mk
>>> index a779250cb..dfbb1d4ae 100644
>>> --- a/controller/automake.mk
>>> +++ b/controller/automake.mk
>>> @@ -73,6 +73,8 @@ controller_ovn_controller_SOURCES = \
>>>
>>> if HAVE_NETLINK
>>> controller_ovn_controller_SOURCES += \
>>> + controller/netlink-utils.h \
>>> + controller/netlink-utils.c \
>>> controller/host-if-monitor.c \
>>> controller/ovn-netlink-notifier.c \
>>> controller/neighbor-exchange-netlink.h \
>>> diff --git a/controller/host-if-monitor.c b/controller/host-if-monitor.c
>>> index 65110d2a3..bbd86aaa5 100644
>>> --- a/controller/host-if-monitor.c
>>> +++ b/controller/host-if-monitor.c
>>> @@ -65,6 +65,21 @@ host_if_monitor_run(void)
>>> return monitor.changes_detected;
>>> }
>>>
>>> +void
>>> +host_if_monitor_invalidate(const char *if_name)
>>> +{
>>> + if (!sset_contains(&monitor.watched_interfaces, if_name)) {
>>> + return;
>>> + }
>>> +
>>> + int32_t ifindex = if_nametoindex(if_name);
>>> + if (ifindex) {
>>> + simap_put(&monitor.ifname_to_ifindex, if_name, ifindex);
>>> + } else {
>>> + simap_find_and_delete(&monitor.ifname_to_ifindex, if_name);
>>> + }
>>> +}
>>> +
>>> void
>>> host_if_monitor_update_watches(const struct sset *if_names)
>>> {
>>> diff --git a/controller/host-if-monitor.h b/controller/host-if-monitor.h
>>> index a41c5869c..f625d839c 100644
>>> --- a/controller/host-if-monitor.h
>>> +++ b/controller/host-if-monitor.h
>>> @@ -22,7 +22,7 @@
>>>
>>> void host_if_monitor_wait(void);
>>> bool host_if_monitor_run(void);
>>> -
>>> +void host_if_monitor_invalidate(const char *if_name);
>>> void host_if_monitor_update_watches(const struct sset *if_names);
>>>
>>> int32_t host_if_monitor_ifname_toindex(const char *if_name);
>>> diff --git a/controller/neighbor-exchange-netlink.c
>>> b/controller/neighbor-exchange-netlink.c
>>> index f01bfda5b..8261f6cf6 100644
>>> --- a/controller/neighbor-exchange-netlink.c
>>> +++ b/controller/neighbor-exchange-netlink.c
>>> @@ -14,8 +14,11 @@
>>> */
>>>
>>> #include <config.h>
>>> +#include <errno.h>
>>> #include <stdbool.h>
>>> +
>>> #include <linux/if_ether.h>
>>> +#include <linux/if_link.h>
>>> #include <linux/rtnetlink.h>
>>>
>>> #include "hmapx.h"
>>> @@ -23,20 +26,13 @@
>>> #include "lib/netlink-socket.h"
>>> #include "lib/packets.h"
>>> #include "openvswitch/vlog.h"
>>> +#include "netlink-utils.h"
>>>
>>> #include "neighbor-exchange-netlink.h"
>>> #include "neighbor.h"
>>>
>>> VLOG_DEFINE_THIS_MODULE(neighbor_exchange_netlink);
>>>
>>> -#define NETNL_REQ_BUFFER_SIZE 128
>>> -
>>> -/* NTF_EXT_LEARNED was introduced in Linux v3.19, define it if
>>> - * not available. */
>>> -#ifndef NTF_EXT_LEARNED
>>> -#define NTF_EXT_LEARNED (1 << 4)
>>> -#endif
>>> -
>>> static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
>>>
>>> /* Inspired from route_table_dump_one_table() in OVS. */
>>> @@ -535,3 +531,94 @@ ne_table_parse(struct ofpbuf *buf, void *change)
>>> return ne_table_parse__(buf, NLMSG_HDRLEN + sizeof *nd,
>>> nlmsg, nd, change);
>>> }
>>> +
>>> +int32_t
>>> +ne_nl_ifindex_get(const char *ifname)
>>> +{
>>> + return nl_ifindex_get(ifname);
>>> +}
>>> +
>>> +int
>>> +ne_nl_create_bridge(const char *ifname)
>>> +{
>>> + return nl_create_device(ifname, "bridge");
>>> +}
>>> +
>>> +int
>>> +ne_nl_create_lo(const char *ifname)
>>> +{
>>> + return nl_create_device(ifname, "dummy");
>>> +}
>>> +
>>> +int
>>> +ne_nl_create_vxlan(const char *ifname, uint32_t vni,
>>> + const struct in6_addr *local_ip, uint16_t dst_port,
>>> + int32_t link_ifindex)
>>> +{
>>> + int err = 0;
>>> + struct ifinfomsg *ifinfo;
>>> + struct ofpbuf request;
>>> + ofpbuf_init(&request, 0);
>>> +
>>> + nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
>>> + NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
>>> NLM_F_EXCL);
>>> + ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
>>> + nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> +
>>> + ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
>>> + size_t linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
>>> + nl_msg_put_string(&request, IFLA_INFO_KIND, "vxlan");
>>> + size_t infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
>>> +
>>> + nl_msg_put_u32(&request, IFLA_VXLAN_ID, vni);
>>> + nl_msg_put_u32(&request, IFLA_VXLAN_LINK, link_ifindex);
>>> + nl_msg_put_u8(&request, IFLA_VXLAN_LEARNING, 0);
>>> + nl_msg_put_be16(&request, IFLA_VXLAN_PORT, htons(dst_port));
>>> +
>>> + if (IN6_IS_ADDR_V4MAPPED(local_ip)) {
>>> + ovs_be32 ipv4 = in6_addr_get_mapped_ipv4(local_ip);
>>> + nl_msg_put_be32(&request, IFLA_VXLAN_LOCAL, ipv4);
>>> + } else {
>>> + nl_msg_put_in6_addr(&request, IFLA_VXLAN_LOCAL6, local_ip);
>>> + }
>>> +
>>> + nl_msg_end_nested(&request, infodata_off);
>>> + nl_msg_end_nested(&request, linkinfo_off);
>>> +
>>> + err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> + ofpbuf_uninit(&request);
>>> + return err;
>>> +}
>>> +
>>> +int
>>> +ne_nl_set_master(const char *slave, const char *master)
>>> +{
>>> + return nl_set_master(slave, master);
>>> +}
>>> +
>>> +int
>>> +ne_nl_set_iface_mac_addr(const char *ifname, const struct eth_addr *mac)
>>> +{
>>> + return nl_set_iface_mac(ifname, mac);
>>> +}
>>> +
>>> +int
>>> +ne_nl_delete_iface(const char *ifname)
>>> +{
>>> + return nl_delete_device(ifname);
>>> +}
>>> +
>>> +int
>>> +ne_nl_create_vrf(const char *ifname, uint32_t table_id)
>>> +{
>>> + if (TABLE_ID_VALID(table_id)) {
>>> + return nl_create_vrf(ifname, table_id);
>>> + }
>>> +
>>> + VLOG_WARN_RL(&rl, "attempt to create VRF using invalid table id
>>> %"PRIu32,
>>> + table_id);
>>> +
>>> + return EINVAL;
>>> +}
>>> +
>>> +
>>> diff --git a/controller/neighbor-exchange-netlink.h
>>> b/controller/neighbor-exchange-netlink.h
>>> index 659fe72d8..37badb226 100644
>>> --- a/controller/neighbor-exchange-netlink.h
>>> +++ b/controller/neighbor-exchange-netlink.h
>>> @@ -50,6 +50,17 @@ struct ne_table_msg {
>>> struct ne_nl_received_neigh nd; /* Data parsed from this message. */
>>> };
>>>
>>> +int32_t ne_nl_ifindex_get(const char *ifname);
>>> +int ne_nl_create_vxlan(const char *ifname, uint32_t vni,
>>> + const struct in6_addr *local_ip, uint16_t dst_port,
>>> + int32_t link_ifindex);
>>> +int ne_nl_create_bridge(const char *ifname);
>>> +int ne_nl_create_lo(const char *ifname);
>>> +int ne_nl_create_vrf(const char *ifname, uint32_t table_id);
>>> +int ne_nl_set_master(const char *slave, const char *master);
>>> +int ne_nl_set_iface_mac_addr(const char *ifname, const struct eth_addr
>>> *mac);
>>> +int ne_nl_delete_iface(const char *ifname);
>>> +
>>> int ne_nl_sync_neigh(uint8_t family, int32_t if_index,
>>> const struct hmap *neighbors,
>>> struct vector *learned_neighbors);
>>> diff --git a/controller/neighbor-exchange-stub.c
>>> b/controller/neighbor-exchange-stub.c
>>> index a1c89ed2b..5a0912b73 100644
>>> --- a/controller/neighbor-exchange-stub.c
>>> +++ b/controller/neighbor-exchange-stub.c
>>> @@ -45,3 +45,13 @@ void
>>> evpn_static_entries_clear(struct hmap *static_entries OVS_UNUSED)
>>> {
>>> }
>>> +
>>> +void
>>> +neighbor_exchange_maintain_evpn_cleanup_all(void)
>>> +{
>>> +}
>>> +
>>> +void
>>> +neighbor_exchange_maintain_evpn_destroy(void)
>>> +{
>>> +}
>>> diff --git a/controller/neighbor-exchange.c b/controller/neighbor-exchange.c
>>> index 43626cc21..b1f6402a5 100644
>>> --- a/controller/neighbor-exchange.c
>>> +++ b/controller/neighbor-exchange.c
>>> @@ -15,12 +15,16 @@
>>>
>>> #include <config.h>
>>>
>>> +#include <errno.h>
>>> #include <linux/neighbour.h>
>>> +#include <net/if.h>
>>>
>>> #include "host-if-monitor.h"
>>> +#include "lib/sset.h"
>>> #include "neighbor.h"
>>> #include "neighbor-exchange.h"
>>> #include "neighbor-exchange-netlink.h"
>>> +#include "route-exchange-netlink.h"
>>> #include "openvswitch/poll-loop.h"
>>> #include "openvswitch/vlog.h"
>>> #include "ovn-util.h"
>>> @@ -47,6 +51,8 @@ static uint32_t evpn_static_entry_hash(const struct
>>> eth_addr *mac,
>>> const struct in6_addr *ip,
>>> uint32_t vni, uint32_t nh_id);
>>>
>>> +static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
>>> +
>>> /* Last neighbor_exchange netlink operation. */
>>> static int neighbor_exchange_nl_status;
>>>
>>> @@ -65,10 +71,328 @@ static int neighbor_exchange_nl_status;
>>> } \
>>> } while (0)
>>>
>>> +struct maintained_evpn_entry {
>>> + struct hmap_node node;
>>> + struct neighbor_ovn_maintain_entry entry;
>>> +};
>>> +
>>> +static struct hmap maintained_evpn_entries =
>>> + HMAP_INITIALIZER(&maintained_evpn_entries);
>>> +
>>> +static struct maintained_evpn_entry *
>>> +maintained_evpn_entry_find(const struct hmap *entries, uint32_t vni)
>>> +{
>>> + struct maintained_evpn_entry *me;
>>> + HMAP_FOR_EACH_WITH_HASH (me, node, hash_int(vni, 0), entries) {
>>> + if (me->entry.vni == vni) {
>>> + return me;
>>> + }
>>> + }
>>> +
>>> + return NULL;
>>> +}
>>> +
>>> +/* Deletes the interfaces for 'entry'. Returns true if all of them were
>>> + * deleted (or were already absent), false if any netlink call failed. */
>>> +static bool
>>> +evpn_delete_evpn_devices(struct neighbor_ovn_maintain_entry *entry)
>>> +{
>>> + int error;
>>> + bool ok = true;
>>> +
>>> + error = ne_nl_delete_iface(entry->br_if_name);
>>> + if (error && error != ENODEV) {
>>> + VLOG_WARN_RL(&rl, "Unable to delete bridge interface %s: %s",
>>> + entry->br_if_name, ovs_strerror(error));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
>>> + ok = false;
>>> + }
>>> +
>>> + error = ne_nl_delete_iface(entry->vxlan_if_name);
>>> + if (error && error != ENODEV) {
>>> + VLOG_WARN_RL(&rl, "Unable to delete VXLAN interface %s: %s",
>>> + entry->vxlan_if_name, ovs_strerror(error));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
>>> + ok = false;
>>> + }
>>> +
>>> + error = ne_nl_delete_iface(entry->vrf_if_name);
>>> + if (error && error != ENODEV) {
>>> + VLOG_WARN_RL(&rl, "Unable to delete vrf interface %s: %s",
>>> + entry->vrf_if_name, ovs_strerror(error));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
>>> + ok = false;
>>> + }
>>> +
>>> + error = ne_nl_delete_iface(entry->lo_if_name);
>>> + if (error && error != ENODEV) {
>>> + VLOG_WARN_RL(&rl, "Unable to delete dummy interface %s: %s",
>>> + entry->lo_if_name, ovs_strerror(error));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(error);
>>> + ok = false;
>>> + }
>>> +
>>> + /* Refresh the host-if-monitor ifindex cache for the interfaces we
>>> + * monitor for neighbor sync, so a lookup later in this same engine
>>> + * iteration doesn't use a stale ifindex. */
>>> + host_if_monitor_invalidate(entry->br_if_name);
>>> + host_if_monitor_invalidate(entry->vxlan_if_name);
>>> + host_if_monitor_invalidate(entry->lo_if_name);
>>> +
>>> + return ok;
>>> +}
>>> +
>>> +static bool
>>> +set_bridge_evpn_device_addr(struct neighbor_ovn_maintain_entry *entry)
>>> +{
>>> + int err;
>>> + if (entry->br_config.has_addr) {
>>> + err = ne_nl_set_iface_mac_addr(entry->br_if_name,
>>> + &entry->br_config.lladdr);
>>> + if (err) {
>>> + VLOG_WARN_RL(&rl, "Unable set mac address on interface %s: %s",
>>> + entry->br_if_name, ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + return false;
>>> + }
>>> + }
>>> +
>>> + return true;
>>> +}
>>> +
>>> +/* Creates the interfaces for 'entry'. Returns true if the whole stack was
>>> + * created successfully (or already existed), false if any netlink call
>>> + * failed. */
>>> +static bool
>>> +evpn_create_devices(struct neighbor_ovn_maintain_entry *entry)
>>> +{
>>> + static char *link_dev = "vxlan_sys_4789";
>>> + int32_t link_ifindex = ne_nl_ifindex_get(link_dev);
>>> + int err;
>>> + bool ok = true;
>>> +
>>> + if (!link_ifindex) {
>>> + err = ENODEV;
>>> + VLOG_WARN_RL(&rl,
>>> + "Unable to find OVS system vxlan interface");
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> +
>>> + if (entry->br_config.has_addr
>>> + && nrm_mode_IP_is_set(entry->redistribute_mode)) {
>>> + err = ne_nl_create_vrf(entry->vrf_if_name, entry->vni);
>>> + if (err && err != EEXIST) {
>>> + VLOG_WARN_RL(&rl,
>>> + "Unable to create VRF %s for datapath %s",
>>> + entry->vrf_if_name, ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> + }
>>> +
>>> + err = ne_nl_create_vxlan(entry->vxlan_if_name, entry->vni,
>>> + &entry->local_ip, entry->fake_vxlan_port,
>>> + link_ifindex);
>>> + if (err && err != EEXIST) {
>>> + VLOG_WARN_RL(&rl,
>>> + "Unable to create VXLAN interface %s for "
>>> + "VNI %"PRIu32": %s",
>>> + entry->vxlan_if_name, entry->vni, ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> +
>>> + err = ne_nl_create_bridge(entry->br_if_name);
>>> + if (err && err != EEXIST) {
>>> + VLOG_WARN_RL(&rl,
>>> + "Unable to create bridge interface %s for "
>>> + "VNI %"PRIu32": %s",
>>> + entry->br_if_name, entry->vni, ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> +
>>> + if (!set_bridge_evpn_device_addr(entry)) {
>>> + ok = false;
>>> + }
>>> +
>>> + if (entry->br_config.has_addr
>>> + && nrm_mode_IP_is_set(entry->redistribute_mode)) {
>>> + err = ne_nl_set_master(entry->br_if_name, entry->vrf_if_name);
>>> + if (err) {
>>> + VLOG_WARN_RL(&rl, "Unable to enslave %s to bridge %s: %s",
>>> + entry->br_if_name, entry->vrf_if_name,
>>> + ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> + }
>>> +
>>> + if (nrm_mode_FDB_is_set(entry->redistribute_mode)) {
>>> + err = ne_nl_create_lo(entry->lo_if_name);
>>> + if (err && err != EEXIST) {
>>> + VLOG_WARN_RL(&rl,
>>> + "Unable to create dummy interface %s for "
>>> + "VNI %"PRIu32": %s",
>>> + entry->lo_if_name, entry->vni, ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> + err = ne_nl_set_master(entry->lo_if_name, entry->br_if_name);
>>> + if (err) {
>>> + VLOG_WARN_RL(&rl, "Unable to enslave %s to bridge %s: %s",
>>> + entry->lo_if_name, entry->br_if_name,
>>> + ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> + }
>>> +
>>> + err = ne_nl_set_master(entry->vxlan_if_name, entry->br_if_name);
>>> + if (err) {
>>> + VLOG_WARN_RL(&rl, "Unable to enslave %s to bridge %s: %s",
>>> + entry->vxlan_if_name, entry->br_if_name,
>>> + ovs_strerror(err));
>>> + SET_NEIGHBOR_EXCHANGE_NL_STATUS(err);
>>> + ok = false;
>>> + }
>>> +
>>> + /* Refresh the host-if-monitor ifindex cache so a lookup later in this
>>> + * same engine iteration picks up the newly (re)created interfaces
>>> + * instead of a stale ifindex left over from before they were
>>> + * deleted/recreated. */
>>> + host_if_monitor_invalidate(entry->br_if_name);
>>> + host_if_monitor_invalidate(entry->vxlan_if_name);
>>> + host_if_monitor_invalidate(entry->lo_if_name);
>>> +
>>> + return ok;
>>> +}
>>> +
>>> +/* Returns true if a change between 'old_entry' and 'entry' requires
>>> + * deleting and recreating the whole VRF/bridge/VXLAN/lo interface stack,
>>> + * rather than updating it in place. This is the case when:
>>> + *
>>> + * - The VXLAN device's local IP or UDP destination port changed: Netlink
>>> + * offers no way to change either on an existing VXLAN interface.
>>> + *
>>> + * - The bridge gained or lost its L3 address ('has_addr'): this changes
>>> + * whether a VRF is needed at all.
>>> + *
>>> + * - The redistribution mode changed (FDB and/or IP bits): this changes
>>> + * whether the loopback advertise interface is needed at all.
>>> + *
>>> + * Recreating the whole stack in these cases is simpler and less
>>> + * error-prone than trying to patch each interface individually. */
>>> +static bool
>>> +should_destroy_interfaces(const struct neighbor_ovn_maintain_entry *entry,
>>> + const struct neighbor_ovn_maintain_entry
>>> *old_entry)
>>> +{
>>> + return !ipv6_addr_equals(&old_entry->local_ip, &entry->local_ip) ||
>>> + old_entry->fake_vxlan_port != entry->fake_vxlan_port ||
>>> + old_entry->br_config.has_addr != entry->br_config.has_addr ||
>>> + old_entry->redistribute_mode != entry->redistribute_mode ||
>>> + (entry->br_config.has_addr
>>> + && !eth_addr_equals(old_entry->br_config.lladdr,
>>> + entry->br_config.lladdr));
>>> +}
>>> +
>>> +static bool
>>> +maintain_evpn_devices(struct neighbor_ovn_maintain_entry *entry,
>>> + struct neighbor_ovn_maintain_entry *old_entry)
>>> +{
>>> + if (!old_entry) {
>>> + return evpn_create_devices(entry);
>>> + }
>>> +
>>> + if (should_destroy_interfaces(entry, old_entry)) {
>>> + if (!evpn_delete_evpn_devices(entry)) {
>>> + return false;
>>> + }
>>> + return evpn_create_devices(entry);
>>> + }
>>> +
>>> + return true;
>>> +}
>>> +
>>> +static void
>>> +neighbor_exchange_maintain_evpn_run(const struct vector *maintain_evpn)
>>> +{
>>> + CLEAR_NEIGHBOR_EXCHANGE_NL_STATUS();
>>> +
>>> + struct hmap old_maintained_evpn_entries =
>>> + HMAP_INITIALIZER(&old_maintained_evpn_entries);
>>> + hmap_swap(&maintained_evpn_entries, &old_maintained_evpn_entries);
>>> +
>>> + struct neighbor_ovn_maintain_entry *entry;
>>> + VECTOR_FOR_EACH_PTR (maintain_evpn, entry) {
>>> + struct maintained_evpn_entry *old_me =
>>> + maintained_evpn_entry_find(&old_maintained_evpn_entries,
>>> + entry->vni);
>>> +
>>> + bool ok = maintain_evpn_devices(entry, old_me ? &old_me->entry :
>>> NULL);
>>> +
>>> + if (!ok && !old_me) {
>>> + /* First-time creation failed: don't start tracking this VNI as
>>> + * maintained. The next run will find no old entry for it and
>>> + * retry evpn_create_devices() from scratch. */
>>> + continue;
>>> + }
>>> +
>>> + struct maintained_evpn_entry *me;
>>> + if (old_me) {
>>> + hmap_remove(&old_maintained_evpn_entries, &old_me->node);
>>> + me = old_me;
>>> + } else {
>>> + me = xmalloc(sizeof *me);
>>> + }
>>> + if (ok) {
>>> + me->entry = *entry;
>>> + }
>>> + hmap_insert(&maintained_evpn_entries, &me->node,
>>> + hash_int(entry->vni, 0));
>>> + }
>>> +
>>> + struct maintained_evpn_entry *stale_me;
>>> + HMAP_FOR_EACH_POP (stale_me, node, &old_maintained_evpn_entries) {
>>> + if (evpn_delete_evpn_devices(&stale_me->entry)) {
>>> + free(stale_me);
>>> + } else {
>>> + /* Deletion failed: keep tracking it so it's retried as stale
>>> + * again on the next run (it's no longer in 'maintain_evpn').
>>> */
>>> + hmap_insert(&maintained_evpn_entries, &stale_me->node,
>>> + hash_int(stale_me->entry.vni, 0));
>>> + }
>>> + }
>>> +
>>> + hmap_destroy(&old_maintained_evpn_entries);
>>> +}
>>> +
>>> +void
>>> +neighbor_exchange_maintain_evpn_cleanup_all(void)
>>> +{
>>> + struct maintained_evpn_entry *me;
>>> + HMAP_FOR_EACH (me, node, &maintained_evpn_entries) {
>>> + evpn_delete_evpn_devices(&me->entry);
>>> + }
>>> +}
>>> +
>>> +void
>>> +neighbor_exchange_maintain_evpn_destroy(void)
>>> +{
>>> + struct maintained_evpn_entry *me;
>>> + HMAP_FOR_EACH_POP (me, node, &maintained_evpn_entries) {
>>> + free(me);
>>> + }
>>> + hmap_destroy(&maintained_evpn_entries);
>>> +}
>>> +
>>> void
>>> neighbor_exchange_run(const struct neighbor_exchange_ctx_in *n_ctx_in,
>>> struct neighbor_exchange_ctx_out *n_ctx_out)
>>> {
>>> + neighbor_exchange_maintain_evpn_run(n_ctx_in->maintain_evpn);
>>> +
>>> struct neighbor_interface_monitor *nim;
>>>
>>> struct sset if_names = SSET_INITIALIZER(&if_names);
>>> diff --git a/controller/neighbor-exchange.h b/controller/neighbor-exchange.h
>>> index 456ff23df..509655788 100644
>>> --- a/controller/neighbor-exchange.h
>>> +++ b/controller/neighbor-exchange.h
>>> @@ -27,6 +27,9 @@ struct unixctl_conn;
>>> struct neighbor_exchange_ctx_in {
>>> /* Contains struct neighbor_interface_monitor pointers. */
>>> const struct vector *monitored_interfaces;
>>> + /* Contains struct neighbor_ovn_maintain_entry for VNIs whose kernel
>>> + * interfaces OVN must create/maintain. May be NULL. */
>>> + const struct vector *maintain_evpn;
>>> };
>>>
>>> struct neighbor_exchange_ctx_out {
>>> @@ -73,4 +76,9 @@ void evpn_remote_vtep_list(struct unixctl_conn *, int
>>> argc,
>>> const char *argv[], void *data_);
>>> void evpn_static_entries_clear(struct hmap *static_entries);
>>>
>>> +/* EVPN kernel interfaces lifecycle. Call cleanup_all on graceful shutdown,
>>> + * destroy to release module-level state. */
>>> +void neighbor_exchange_maintain_evpn_cleanup_all(void);
>>> +void neighbor_exchange_maintain_evpn_destroy(void);
>>> +
>>> #endif /* NEIGHBOR_EXCHANGE_H */
>>> diff --git a/controller/neighbor.c b/controller/neighbor.c
>>> index ada9254e0..2c42ec541 100644
>>> --- a/controller/neighbor.c
>>> +++ b/controller/neighbor.c
>>> @@ -27,6 +27,7 @@
>>> #include "neighbor.h"
>>>
>>> VLOG_DEFINE_THIS_MODULE(neighbor);
>>> +static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>>>
>>> static const char *neighbor_opt_name[] = {
>>> [NEIGH_IFACE_BRIDGE] = "dynamic-routing-bridge-ifname",
>>> @@ -88,13 +89,111 @@ neigh_parse_device_name(struct sset *device_names,
>>> struct local_datapath *ld,
>>> neighbor_opt_name[type], "");
>>> sset_from_delimited_string(device_names, names, ",");
>>> if (sset_is_empty(device_names)) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>>> VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" misses %s",
>>> UUID_ARGS(&ld->datapath->header_.uuid),
>>> neighbor_opt_name[type]);
>>> }
>>> }
>>>
>>> +static bool
>>> +create_maintain_evpn_entry(const struct neighbor_ctx_in *n_ctx_in,
>>> + const struct local_datapath *ld, uint32_t vni,
>>> + enum neigh_redistribute_mode redistribute_mode,
>>> + enum neighbor_mainatain_mode mainatain_mode,
>>> + struct neighbor_ovn_maintain_entry *entry)
>>> +{
>>> + if (vni > UINT16_MAX - 60000) {
>>> + VLOG_WARN_RL(&rl, "dynamic-routing-vni %"PRIu32" for datapath "
>>> + UUID_FMT" is too large for
>>> dynamic-routing-maintain-evpn "
>>> + "(max supported VNI is %u)",
>>> + vni, UUID_ARGS(&ld->datapath->header_.uuid),
>>> + UINT16_MAX - 60000);
>>> + return false;
>>> + }
>>> +
>>> + *entry = (struct neighbor_ovn_maintain_entry) {
>>> + .redistribute_mode = redistribute_mode,
>>> + .mainatain_mode = mainatain_mode,
>>> + .vni = vni,
>>> + .fake_vxlan_port = 60000 + vni,
>>> + };
>>> +
>>> + snprintf(entry->vxlan_if_name, sizeof entry->vxlan_if_name,
>>> + "ovnvxlan%"PRIu32, vni);
>>> + snprintf(entry->br_if_name, sizeof entry->br_if_name,
>>> + "ovnbr%"PRIu32, vni);
>>> + snprintf(entry->lo_if_name, sizeof entry->lo_if_name,
>>> + "ovnlo%"PRIu32, vni);
>>> + snprintf(entry->vrf_if_name, sizeof entry->vrf_if_name,
>>> + "ovnvrf%"PRIu32, vni);
>>> +
>>> + const struct in6_addr *local_ip = NULL;
>>> + local_ip = evpn_local_ip_map_lookup(n_ctx_in->evpn_local_ip_map,
>>> + vni, true);
>>> + if (!local_ip) {
>>> + local_ip = evpn_local_ip_map_lookup(n_ctx_in->evpn_local_ip_map,
>>> + vni, false);
>>> + }
>>> +
>>> + if (local_ip) {
>>> + entry->local_ip = *local_ip;
>>> + } else {
>>> + VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn set for datapath "
>>> + UUID_FMT" VNI %"PRIu32" but no ovn-evpn-local-ip "
>>> + "configured",
>>> + UUID_ARGS(&ld->datapath->header_.uuid), vni);
>>> + return false;
>>> + }
>>> +
>>> + if (nrm_mode_IP_is_set(redistribute_mode)) {
>>> + const char *lrp_name = smap_get(&ld->datapath->external_ids,
>>> +
>>> "dynamic-routing-maintain-evpn-lrp");
>>> + if (!lrp_name) {
>>> + VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn=mvd is set
>>> with "
>>> + "IP redistribution on datapath "UUID_FMT" but "
>>> + "dynamic-routing-maintain-evpn-lrp is not
>>> configured",
>>> + UUID_ARGS(&ld->datapath->header_.uuid));
>>> + return false;
>>> + }
>>> +
>>> + const struct sbrec_port_binding *pb =
>>> + lport_lookup_by_name(n_ctx_in->sbrec_pb_by_name, lrp_name);
>>> + if (!pb) {
>>> + VLOG_WARN_RL(&rl, "dynamic-routing-evpn-lrp: port \"%s\" "
>>> + "not found", lrp_name);
>>> + return false;
>>> + }
>>> +
>>> + entry->br_config.has_addr =
>>> + extract_sbrec_binding_first_mac(pb, &entry->br_config.lladdr);
>>> + if (!entry->br_config.has_addr) {
>>> + VLOG_WARN_RL(&rl, "Unable to parse mac address on router "
>>> + "port %s for configuring EVPN devices", lrp_name);
>>> + return false;
>>> + }
>>> + }
>>> +
>>> + return true;
>>> +}
>>> +
>>> +static enum neighbor_mainatain_mode
>>> +parse_neigh_maintain_mode(const struct smap *external_ids)
>>> +{
>>> + const char *val = smap_get(external_ids,
>>> "dynamic-routing-maintain-evpn");
>>> + if (!val) {
>>> + return NEIGH_MAINTAIN_NONE;
>>> + }
>>> +
>>> + if (!strcmp(val, "mvd")) {
>>> + return NEIGH_MAINTAIN_MVD;
>>> + }
>>> +
>>> + VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn: unknown value
>>> \"%s\", "
>>> + "expected \"mvd\"", val);
>>> +
>>> + return NEIGH_MAINTAIN_NONE;
>>> +}
>>> +
>>> void
>>> neighbor_run(struct neighbor_ctx_in *n_ctx_in,
>>> struct neighbor_ctx_out *n_ctx_out)
>>> @@ -116,24 +215,50 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
>>> continue;
>>> }
>>>
>>> + enum neigh_redistribute_mode redistribute_mode =
>>> + parse_neigh_dynamic_redistribute(&ld->datapath->external_ids);
>>> +
>>> + enum neighbor_mainatain_mode mainatain_mode =
>>> + parse_neigh_maintain_mode(&ld->datapath->external_ids);
>>> +
>>> + struct neighbor_ovn_maintain_entry maintain_entry;
>>> + if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
>>> + if (create_maintain_evpn_entry(n_ctx_in, ld, (uint32_t) vni,
>>> + redistribute_mode,
>>> mainatain_mode,
>>> + &maintain_entry)) {
>>> + vector_push(n_ctx_out->maintain_evpn, &maintain_entry);
>>> + } else {
>>> + continue;
>>> + }
>>> + }
>>> +
>>> struct sset device_names;
>>> - neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_VXLAN);
>>> + if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
>>> + sset_init(&device_names);
>>> + sset_add(&device_names, maintain_entry.vxlan_if_name);
>>> + } else {
>>> + neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_VXLAN);
>>> + }
>>> const char *name;
>>> SSET_FOR_EACH (name, &device_names) {
>>> - struct neighbor_interface_monitor *vxlan =
>>> + struct neighbor_interface_monitor *nim =
>>> neighbor_interface_monitor_alloc(NEIGH_AF_BRIDGE,
>>> NEIGH_IFACE_VXLAN, vni,
>>> name);
>>> - vector_push(n_ctx_out->monitored_interfaces, &vxlan);
>>> + vector_push(n_ctx_out->monitored_interfaces, &nim);
>>> }
>>> sset_destroy(&device_names);
>>>
>>> struct neighbor_interface_monitor *lo = NULL;
>>> - neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_LOOPBACK);
>>> - if (sset_count(&device_names) > 1) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>>> - VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names provided
>>> "
>>> - "for loopback device",
>>> - UUID_ARGS(&ld->datapath->header_.uuid));
>>> + if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
>>> + sset_init(&device_names);
>>> + sset_add(&device_names, maintain_entry.lo_if_name);
>>> + } else {
>>> + neigh_parse_device_name(&device_names, ld,
>>> NEIGH_IFACE_LOOPBACK);
>>> + if (sset_count(&device_names) > 1) {
>>> + VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names for "
>>> + "loopback device",
>>> + UUID_ARGS(&ld->datapath->header_.uuid));
>>> + }
>>> }
>>>
>>> if (!sset_is_empty(&device_names)) {
>>> @@ -146,32 +271,31 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
>>>
>>> struct neighbor_interface_monitor *br_v4 = NULL;
>>> struct neighbor_interface_monitor *br_v6 = NULL;
>>> - neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_BRIDGE);
>>> - if (sset_count(&device_names) > 1) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>>> - VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names provided
>>> "
>>> - "for bridge device",
>>> - UUID_ARGS(&ld->datapath->header_.uuid));
>>> + if (mainatain_mode != NEIGH_MAINTAIN_NONE) {
>>> + sset_init(&device_names);
>>> + sset_add(&device_names, maintain_entry.br_if_name);
>>> + } else {
>>> + neigh_parse_device_name(&device_names, ld, NEIGH_IFACE_BRIDGE);
>>> + if (sset_count(&device_names) > 1) {
>>> + VLOG_WARN_RL(&rl, "Datapath "UUID_FMT" too many names for "
>>> + "bridge device",
>>> + UUID_ARGS(&ld->datapath->header_.uuid));
>>> + }
>>> }
>>>
>>> if (!sset_is_empty(&device_names)) {
>>> - br_v4 =
>>> - neighbor_interface_monitor_alloc(NEIGH_AF_INET,
>>> - NEIGH_IFACE_BRIDGE, vni,
>>> -
>>> SSET_FIRST(&device_names));
>>> + br_v4 = neighbor_interface_monitor_alloc(
>>> + NEIGH_AF_INET, NEIGH_IFACE_BRIDGE, vni,
>>> + SSET_FIRST(&device_names));
>>> vector_push(n_ctx_out->monitored_interfaces, &br_v4);
>>> -
>>> - br_v6 =
>>> - neighbor_interface_monitor_alloc(NEIGH_AF_INET6,
>>> - NEIGH_IFACE_BRIDGE, vni,
>>> -
>>> SSET_FIRST(&device_names));
>>> + br_v6 = neighbor_interface_monitor_alloc(
>>> + NEIGH_AF_INET6, NEIGH_IFACE_BRIDGE, vni,
>>> + SSET_FIRST(&device_names));
>>> vector_push(n_ctx_out->monitored_interfaces, &br_v6);
>>> }
>>> sset_destroy(&device_names);
>>>
>>> - enum neigh_redistribute_mode mode =
>>> - parse_neigh_dynamic_redistribute(&ld->datapath->external_ids);
>>> - if (nrm_mode_FDB_is_set(mode) && lo) {
>>> + if (nrm_mode_FDB_is_set(redistribute_mode) && lo) {
>>> neighbor_collect_mac_to_advertise(n_ctx_in,
>>> &lo->announced_neighbors,
>>> n_ctx_out->advertised_pbs,
>>> @@ -182,7 +306,7 @@ neighbor_run(struct neighbor_ctx_in *n_ctx_in,
>>> * meant to be advertised. With 'fdb' set, also program them as
>>> FDB
>>> * entries. */
>>> neighbor_collect_advertised_mac_bindings(
>>> - n_ctx_in, mode, lo ? &lo->announced_neighbors : NULL,
>>> + n_ctx_in, redistribute_mode, lo ? &lo->announced_neighbors :
>>> NULL,
>>> br_v4 ? &br_v4->announced_neighbors : NULL,
>>> br_v6 ? &br_v6->announced_neighbors : NULL,
>>> n_ctx_out->advertised_pbs, ld->datapath);
>>> diff --git a/controller/neighbor.h b/controller/neighbor.h
>>> index e3adc87d1..bb440535f 100644
>>> --- a/controller/neighbor.h
>>> +++ b/controller/neighbor.h
>>> @@ -23,6 +23,7 @@
>>>
>>> #include "lib/sset.h"
>>> #include "openvswitch/hmap.h"
>>> +#include "lib/ovn-util.h"
>>>
>>> #include "vec.h"
>>>
>>> @@ -49,6 +50,7 @@ struct neighbor_ctx_in {
>>> /* Index for Port Binding by name. */
>>> struct ovsdb_idl_index *sbrec_pb_by_name;
>>> const struct sbrec_chassis *chassis;
>>> + struct evpn_local_ip_map *evpn_local_ip_map;
>>> };
>>>
>>> struct neighbor_ctx_out {
>>> @@ -56,6 +58,10 @@ struct neighbor_ctx_out {
>>> struct vector *monitored_interfaces;
>>> /* Contains set of port binding names that are currently advertised.
>>> */
>>> struct sset *advertised_pbs;
>>> + /* Contains struct neighbor_ovn_maintain_entry for VNIs that need
>>> + * maintained interfaces. May be NULL if caller does not need this. */
>>> + struct vector *maintain_evpn;
>>> + struct evpn_local_ip_map *evpn_ip_map;
>>> };
>>>
>>> enum neighbor_interface_type {
>>> @@ -64,6 +70,34 @@ enum neighbor_interface_type {
>>> NEIGH_IFACE_LOOPBACK,
>>> };
>>>
>>> +enum neighbor_mainatain_mode {
>>> + NEIGH_MAINTAIN_NONE,
>>> + NEIGH_MAINTAIN_MVD,
>>> +};
>>> +
>>> +/* IP/MAC to configure on the bridge interface so BGP can use it as
>>> next-hop.
>>> + * Filled from the LRP named by dynamic-routing-evpn-lrp. */
>>> +struct neighbor_bridge_iface_config {
>>> + bool has_addr;
>>> + struct eth_addr lladdr;
>>> + struct in6_addr addr; /* IPv4 stored as mapped IPv4-in-IPv6. */
>>> + uint8_t prefix_len;
>>> +};
>>> +
>>> +struct neighbor_ovn_maintain_entry {
>>> + enum neigh_redistribute_mode redistribute_mode;
>>> + enum neighbor_mainatain_mode mainatain_mode;
>>> + char vxlan_if_name[IFNAMSIZ + 1];
>>> + char vrf_if_name[IFNAMSIZ + 1];
>>> + char br_if_name[IFNAMSIZ + 1];
>>> + char lo_if_name[IFNAMSIZ + 1];
>>> + struct neighbor_bridge_iface_config br_config;
>>> + struct in6_addr local_ip;
>>> + uint32_t vni;
>>> + /* Formula: 60000 + vni (valid for VNI <= 5535). */
>>> + uint16_t fake_vxlan_port;
>>> +};
>>> +
>>> struct neighbor_interface_monitor {
>>> enum neighbor_family family;
>>> char if_name[IFNAMSIZ + 1];
>>> diff --git a/controller/netlink-utils.c b/controller/netlink-utils.c
>>> new file mode 100644
>>> index 000000000..4055f8809
>>> --- /dev/null
>>> +++ b/controller/netlink-utils.c
>>> @@ -0,0 +1,148 @@
>>> +/* Copyright (c) 2025, Red Hat, Inc.
>>> + *
>>> + * 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 <stdbool.h>
>>> +
>>> +#include <errno.h>
>>> +#include <linux/if_ether.h>
>>> +#include <linux/if_link.h>
>>> +#include <linux/rtnetlink.h>
>>> +
>>> +#include "lib/netlink.h"
>>> +#include "lib/netlink-socket.h"
>>> +#include "lib/packets.h"
>>> +
>>> +#include "openvswitch/ofpbuf.h"
>>> +
>>> +#include "netlink-utils.h"
>>> +
>>> +int32_t
>>> +nl_ifindex_get(const char *ifname)
>>> +{
>>> + return (int32_t) if_nametoindex(ifname);
>>> +}
>>> +
>>> +int
>>> +nl_create_device(const char *ifname, const char *kind)
>>> +{
>>> + struct ifinfomsg *ifinfo;
>>> + struct ofpbuf request;
>>> + ofpbuf_init(&request, 0);
>>> +
>>> + nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
>>> + NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
>>> NLM_F_EXCL);
>>> + ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
>>> + nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> + ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
>>> +
>>> + size_t linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
>>> + nl_msg_put_string(&request, IFLA_INFO_KIND, kind);
>>> + nl_msg_end_nested(&request, linkinfo_off);
>>> +
>>> + int err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> + ofpbuf_uninit(&request);
>>> + return err;
>>> +}
>>> +
>>> +int
>>> +nl_create_vrf(const char *ifname, uint32_t table_id)
>>> +{
>>> + size_t linkinfo_off, infodata_off;
>>> + struct ifinfomsg *ifinfo;
>>> + int err;
>>> +
>>> + struct ofpbuf request;
>>> + uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
>>> + ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));
>>> +
>>> + nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
>>> + NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
>>> NLM_F_EXCL);
>>> + ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
>>> + nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> +
>>> + ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
>>> + linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
>>> + nl_msg_put_string(&request, IFLA_INFO_KIND, "vrf");
>>> + infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
>>> + nl_msg_put_u32(&request, IFLA_VRF_TABLE, table_id);
>>> + nl_msg_end_nested(&request, infodata_off);
>>> + nl_msg_end_nested(&request, linkinfo_off);
>>> +
>>> + err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> +
>>> + ofpbuf_uninit(&request);
>>> + return err;
>>> +}
>>> +
>>> +int
>>> +nl_delete_device(const char *ifname)
>>> +{
>>> + struct ifinfomsg *ifinfo;
>>> + int err;
>>> +
>>> + struct ofpbuf request;
>>> + uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
>>> + ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));
>>> +
>>> + nl_msg_put_nlmsghdr(&request, 0, RTM_DELLINK, NLM_F_REQUEST |
>>> NLM_F_ACK);
>>> + ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
>>> + nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> + err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> +
>>> + ofpbuf_uninit(&request);
>>> + return err;
>>> +}
>>> +
>>> +int
>>> +nl_set_master(const char *slave, const char *master)
>>> +{
>>> + int err = 0;
>>> + int32_t master_idx = nl_ifindex_get(master);
>>> + if (!master_idx) {
>>> + return ENODEV;
>>> + }
>>> +
>>> + struct ofpbuf request;
>>> + ofpbuf_init(&request, 0);
>>> +
>>> + nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
>>> + NLM_F_REQUEST | NLM_F_ACK);
>>> + ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
>>> + nl_msg_put_string(&request, IFLA_IFNAME, slave);
>>> + nl_msg_put_u32(&request, IFLA_MASTER, master_idx);
>>> +
>>> + err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> + ofpbuf_uninit(&request);
>>> + return err;
>>> +}
>>> +
>>> +int
>>> +nl_set_iface_mac(const char *ifname, const struct eth_addr *mac)
>>> +{
>>> + int err = 0;
>>> + struct ofpbuf request;
>>> + ofpbuf_init(&request, 0);
>>> +
>>> + nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
>>> + NLM_F_REQUEST | NLM_F_ACK);
>>> + ofpbuf_put_zeros(&request, sizeof(struct ifinfomsg));
>>> + nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> + nl_msg_put_unspec(&request, IFLA_ADDRESS, mac, sizeof *mac);
>>> +
>>> + err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> + ofpbuf_uninit(&request);
>>> + return err;
>>> +}
>>> diff --git a/controller/netlink-utils.h b/controller/netlink-utils.h
>>> new file mode 100644
>>> index 000000000..95c817e33
>>> --- /dev/null
>>> +++ b/controller/netlink-utils.h
>>> @@ -0,0 +1,41 @@
>>> +/*
>>> + * Copyright (c) 2025 Canonical, Ltd.
>>> + * Copyright (c) 2025, STACKIT GmbH & Co. KG
>>> + *
>>> + * 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_UTILS_H
>>> +#define NETLINK_UTILS_H 1
>>> +
>>> +#include <stdint.h>
>>> +#include <linux/rtnetlink.h>
>>> +#include <netinet/in.h>
>>> +#include <net/if.h>
>>> +
>>> +#define NETNL_REQ_BUFFER_SIZE 128
>>> +
>>> +#define TABLE_ID_VALID(table_id) (table_id != RT_TABLE_UNSPEC &&
>>> \
>>> + table_id != RT_TABLE_COMPAT &&
>>> \
>>> + table_id != RT_TABLE_LOCAL &&
>>> \
>>> + table_id != RT_TABLE_MAX)
>>> +
>>> +int32_t nl_ifindex_get(const char *ifname);
>>> +int nl_create_device(const char *ifname, const char *kind);
>>> +int nl_create_vrf(const char *ifname, uint32_t table_id);
>>> +int nl_delete_device(const char *ifname);
>>> +int nl_set_iface_mac(const char *ifname,
>>> + const struct eth_addr *mac);
>>> +int nl_set_master(const char *slave, const char *master);
>>> +
>>> +#endif /* netlink-utils.h */
>>> diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
>>> index 5f63c5fec..dc6089388 100644
>>> --- a/controller/ovn-controller.c
>>> +++ b/controller/ovn-controller.c
>>> @@ -6081,6 +6081,9 @@ struct ed_type_neighbor {
>>> struct vector monitored_interfaces;
>>> /* Contains set of PB names that are currently advertised. */
>>> struct sset advertised_pbs;
>>> + /* Contains struct neighbor_ovn_maintain_entry, one per VNI
>>> + * whose EVPN interfaces are auto-created and maintained. */
>>> + struct vector maintain_evpn;
>>> };
>>>
>>> static void *
>>> @@ -6093,6 +6096,8 @@ en_neighbor_init(struct engine_node *node OVS_UNUSED,
>>> .monitored_interfaces =
>>> VECTOR_EMPTY_INITIALIZER(struct neighbor_interface_monitor *),
>>> .advertised_pbs = SSET_INITIALIZER(&data->advertised_pbs),
>>> + .maintain_evpn =
>>> + VECTOR_EMPTY_INITIALIZER(struct neighbor_ovn_maintain_entry),
>>> };
>>> return data;
>>> }
>>> @@ -6104,6 +6109,7 @@ en_neighbor_cleanup(void *data)
>>>
>>> neighbor_cleanup(&ne_data->monitored_interfaces);
>>> vector_destroy(&ne_data->monitored_interfaces);
>>> + vector_destroy(&ne_data->maintain_evpn);
>>> sset_destroy(&ne_data->advertised_pbs);
>>> }
>>>
>>> @@ -6138,22 +6144,32 @@ en_neighbor_run(struct engine_node *node
>>> OVS_UNUSED, void *data)
>>> chassis_lookup_by_name(sbrec_chassis_by_name, chassis_id);
>>> ovs_assert(chassis);
>>>
>>> + struct evpn_local_ip_map evpn_ip_map = {
>>> + .vni_ip4 = HMAP_INITIALIZER(&evpn_ip_map.vni_ip4),
>>> + .vni_ip6 = HMAP_INITIALIZER(&evpn_ip_map.vni_ip6),
>>> + };
>>> + evpn_local_ip_map_init(&evpn_ip_map, &chassis->other_config);
>>> +
>>> struct neighbor_ctx_in n_ctx_in = {
>>> .local_datapaths = &rt_data->local_datapaths,
>>> .sbrec_pb_by_dp = sbrec_port_binding_by_datapath,
>>> .sbrec_amb_by_dp = sbrec_advertised_mac_binding_by_datapath,
>>> .sbrec_pb_by_name = sbrec_port_binding_by_name,
>>> .chassis = chassis,
>>> + .evpn_local_ip_map = &evpn_ip_map,
>>> };
>>>
>>> struct neighbor_ctx_out n_ctx_out = {
>>> .monitored_interfaces = &ne_data->monitored_interfaces,
>>> .advertised_pbs = &ne_data->advertised_pbs,
>>> + .maintain_evpn = &ne_data->maintain_evpn,
>>> };
>>>
>>> neighbor_cleanup(&ne_data->monitored_interfaces);
>>> sset_clear(&ne_data->advertised_pbs);
>>> + vector_clear(&ne_data->maintain_evpn);
>>> neighbor_run(&n_ctx_in, &n_ctx_out);
>>> + evpn_local_ip_map_destroy(&evpn_ip_map);
>>>
>>> return EN_UPDATED;
>>> }
>>> @@ -6486,6 +6502,7 @@ en_neighbor_exchange_run(struct engine_node *node,
>>> void *data_)
>>>
>>> struct neighbor_exchange_ctx_in n_ctx_in = {
>>> .monitored_interfaces = &neighbor_data->monitored_interfaces,
>>> + .maintain_evpn = &neighbor_data->maintain_evpn,
>>> };
>>> struct neighbor_exchange_ctx_out n_ctx_out = {
>>> .neighbor_table_watches = &nt_notify->watches,
>>> @@ -8465,6 +8482,8 @@ loop_done:
>>> route_exchange_cleanup_vrfs();
>>> }
>>>
>>> + neighbor_exchange_maintain_evpn_cleanup_all();
>>> +
>>> /* The engine cleanup should happen only after threads have been
>>> * destroyed and joined in case they are accessing engine data. */
>>> pinctrl_destroy();
>>> @@ -8501,6 +8520,7 @@ loop_done:
>>> dns_resolve_destroy();
>>> route_exchange_destroy();
>>> ovn_netlink_notifiers_destroy();
>>> + neighbor_exchange_maintain_evpn_destroy();
>>>
>>> exit(retval);
>>> }
>>> diff --git a/controller/physical.c b/controller/physical.c
>>> index 89ff2785e..0277d472d 100644
>>> --- a/controller/physical.c
>>> +++ b/controller/physical.c
>>> @@ -3231,163 +3231,6 @@ physical_eval_remote_chassis_flows(const struct
>>> physical_ctx *ctx,
>>> ofpbuf_uninit(&ingress_ofpacts);
>>> }
>>>
>>> -struct vni_local_ip {
>>> - struct hmap_node hmap_node;
>>> - struct in6_addr ip;
>>> - uint32_t vni;
>>> -};
>>> -
>>> -struct evpn_local_ip_map {
>>> - struct hmap vni_ip4; /* Per VNI local IPv4 vni_local_ips. */
>>> - struct hmap vni_ip6; /* Per VNI local IPv6 vni_local_ips. */
>>> - struct in6_addr default_ip4; /* Default local IPv4. */
>>> - struct in6_addr default_ip6; /* Default local IPv6. */
>>> -};
>>> -
>>> -static const struct in6_addr *
>>> -evpn_local_ip_lookup(const struct hmap *map, uint32_t vni)
>>> -{
>>> - struct vni_local_ip *e;
>>> - HMAP_FOR_EACH_WITH_HASH (e, hmap_node, hash_add(vni, 0), map) {
>>> - if (e->vni == vni) {
>>> - return &e->ip;
>>> - }
>>> - }
>>> - return NULL;
>>> -}
>>> -
>>> -static ovs_be32
>>> -evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
>>> - uint32_t vni)
>>> -{
>>> - const struct in6_addr *addr =
>>> evpn_local_ip_lookup(&vni_ip_map->vni_ip4,
>>> - vni);
>>> - if (addr) {
>>> - return in6_addr_get_mapped_ipv4(addr);
>>> - }
>>> -
>>> - if (ipv6_addr_is_set(&vni_ip_map->default_ip4)) {
>>> - return in6_addr_get_mapped_ipv4(&vni_ip_map->default_ip4);
>>> - }
>>> -
>>> - return 0;
>>> -}
>>> -
>>> -static const struct in6_addr *
>>> -evpn_local_ip_find_v6(const struct evpn_local_ip_map *vni_ip_map,
>>> - uint32_t vni)
>>> -{
>>> - const struct in6_addr *addr =
>>> evpn_local_ip_lookup(&vni_ip_map->vni_ip6,
>>> - vni);
>>> - if (addr) {
>>> - return addr;
>>> - }
>>> -
>>> - if (ipv6_addr_is_set(&vni_ip_map->default_ip6)) {
>>> - return &vni_ip_map->default_ip6;
>>> - }
>>> -
>>> - return NULL;
>>> -}
>>> -
>>> -static void
>>> -evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
>>> - const struct smap *config)
>>> -{
>>> - char *tokstr, *token, *ptr0 = NULL;
>>> -
>>> - const char *local_ip_str = smap_get_def(config, "ovn-evpn-local-ip",
>>> "");
>>> - tokstr = xstrdup(local_ip_str);
>>> - for (token = strtok_r(tokstr, ",", &ptr0); token;
>>> - token = strtok_r(NULL, ",", &ptr0)) {
>>> - char *ptr1 = NULL, *vni_str = strtok_r(token, "-", &ptr1);
>>> - char *ip_str = strtok_r(NULL, "-", &ptr1);
>>> - struct in6_addr ip;
>>> - uint32_t vni;
>>> -
>>> - if (ptr1 && *ptr1) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
>>> - VLOG_WARN_RL(&rl, "Malformed 'ovn-evpn-local-ip': %s", tokstr);
>>> - break;
>>> - }
>>> -
>>> - if (!ip_str) { /* default IP */
>>> - if (!ip46_parse(vni_str, &ip)) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> - VLOG_WARN_RL(&rl, "Invalid IP %s in 'ovn-evpn-local-ip'",
>>> - vni_str);
>>> - continue;
>>> - }
>>> - struct in6_addr *ip_ptr = IN6_IS_ADDR_V4MAPPED(&ip)
>>> - ? &vni_ip_map->default_ip4
>>> - : &vni_ip_map->default_ip6;
>>> - if (ipv6_addr_is_set(ip_ptr)) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> - VLOG_WARN_RL(&rl, "Default ovn-evpn-local-ip IP%d "
>>> - "already configured",
>>> - IN6_IS_ADDR_V4MAPPED(&ip) ? 4 : 6);
>>> - continue;
>>> - }
>>> - *ip_ptr = ip;
>>> - } else {
>>> - if (!ip46_parse(ip_str, &ip)) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> - VLOG_WARN_RL(&rl,
>>> - "EVPN enabled, but required
>>> 'ovn-evpn-local-ip' "
>>> - "is missing or invalid for vni %s", vni_str);
>>> - continue;
>>> - }
>>> -
>>> - if (!vni_str) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> - VLOG_WARN_RL(&rl, "Required 'ovn-evpn-local-ip' VNI not "
>>> - "configured for IP %s",
>>> - ip_str);
>>> - continue;
>>> - }
>>> -
>>> - if (!ovs_scan(vni_str, "%u", &vni) || !ovn_is_valid_vni(vni)) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> - VLOG_WARN_RL(&rl, "Invalid VNI %s in 'ovn-evpn-local-ip'",
>>> - vni_str);
>>> - continue;
>>> - }
>>> -
>>> - struct hmap *map = IN6_IS_ADDR_V4MAPPED(&ip)
>>> - ? &vni_ip_map->vni_ip4
>>> - : &vni_ip_map->vni_ip6;
>>> - if (evpn_local_ip_lookup(map, vni)) {
>>> - static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> - VLOG_WARN_RL(&rl, "Duplicated VNI entry: %s", vni_str);
>>> - continue;
>>> - }
>>> -
>>> - struct vni_local_ip *e = xmalloc(sizeof *e);
>>> - *e = (struct vni_local_ip) {
>>> - .vni = vni,
>>> - .ip = ip,
>>> - };
>>> - hmap_insert(map, &e->hmap_node, hash_add(vni, 0));
>>> - }
>>> - }
>>> - free(tokstr);
>>> -}
>>> -
>>> -static void
>>> -evpn_local_ip_map_destroy(struct evpn_local_ip_map *map)
>>> -{
>>> - struct vni_local_ip *e;
>>> - HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip4) {
>>> - free(e);
>>> - }
>>> - hmap_destroy(&map->vni_ip4);
>>> -
>>> - HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip6) {
>>> - free(e);
>>> - }
>>> - hmap_destroy(&map->vni_ip6);
>>> -}
>>> -
>>> static void
>>> physical_consider_evpn_binding(const struct evpn_binding *binding,
>>> const struct evpn_local_ip_map *vni_ip_map,
>>> diff --git a/controller/route-exchange-netlink.c
>>> b/controller/route-exchange-netlink.c
>>> index 8f1615c4e..e280fae1d 100644
>>> --- a/controller/route-exchange-netlink.c
>>> +++ b/controller/route-exchange-netlink.c
>>> @@ -31,13 +31,12 @@
>>> #include "route-table.h"
>>> #include "route.h"
>>> #include "vec.h"
>>> +#include "netlink-utils.h"
>>>
>>> #include "route-exchange-netlink.h"
>>>
>>> VLOG_DEFINE_THIS_MODULE(route_exchange_netlink);
>>>
>>> -#define NETNL_REQ_BUFFER_SIZE 128
>>> -
>>> static void re_nl_encode_nexthop(struct ofpbuf *, bool dst_is_ipv4,
>>> const struct in6_addr *);
>>>
>>> @@ -51,51 +50,13 @@ re_nl_create_vrf(const char *ifname, uint32_t table_id)
>>> table_id);
>>> return EINVAL;
>>> }
>>> -
>>> - size_t linkinfo_off, infodata_off;
>>> - struct ifinfomsg *ifinfo;
>>> - int err;
>>> -
>>> - struct ofpbuf request;
>>> - uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
>>> - ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));
>>> -
>>> - nl_msg_put_nlmsghdr(&request, 0, RTM_NEWLINK,
>>> - NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE |
>>> NLM_F_EXCL);
>>> - ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
>>> - nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> -
>>> - ifinfo->ifi_change = ifinfo->ifi_flags = IFF_UP;
>>> - linkinfo_off = nl_msg_start_nested(&request, IFLA_LINKINFO);
>>> - nl_msg_put_string(&request, IFLA_INFO_KIND, "vrf");
>>> - infodata_off = nl_msg_start_nested(&request, IFLA_INFO_DATA);
>>> - nl_msg_put_u32(&request, IFLA_VRF_TABLE, table_id);
>>> - nl_msg_end_nested(&request, infodata_off);
>>> - nl_msg_end_nested(&request, linkinfo_off);
>>> -
>>> - err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> -
>>> - ofpbuf_uninit(&request);
>>> - return err;
>>> + return nl_create_vrf(ifname, table_id);
>>> }
>>>
>>> int
>>> re_nl_delete_vrf(const char *ifname)
>>> {
>>> - struct ifinfomsg *ifinfo;
>>> - int err;
>>> -
>>> - struct ofpbuf request;
>>> - uint8_t request_stub[NETNL_REQ_BUFFER_SIZE];
>>> - ofpbuf_use_stub(&request, request_stub, sizeof(request_stub));
>>> -
>>> - nl_msg_put_nlmsghdr(&request, 0, RTM_DELLINK, NLM_F_REQUEST |
>>> NLM_F_ACK);
>>> - ifinfo = ofpbuf_put_zeros(&request, sizeof *ifinfo);
>>> - nl_msg_put_string(&request, IFLA_IFNAME, ifname);
>>> - err = nl_transact(NETLINK_ROUTE, &request, NULL);
>>> -
>>> - ofpbuf_uninit(&request);
>>> - return err;
>>> + return nl_delete_device(ifname);
>>> }
>>>
>>> void
>>> diff --git a/controller/route-exchange-netlink.h
>>> b/controller/route-exchange-netlink.h
>>> index c137b5119..b6718f3c7 100644
>>> --- a/controller/route-exchange-netlink.h
>>> +++ b/controller/route-exchange-netlink.h
>>> @@ -29,11 +29,6 @@
>>> #define RTPROT_OVN 84
>>> #endif
>>>
>>> -#define TABLE_ID_VALID(table_id) (table_id != RT_TABLE_UNSPEC &&
>>> \
>>> - table_id != RT_TABLE_COMPAT &&
>>> \
>>> - table_id != RT_TABLE_LOCAL &&
>>> \
>>> - table_id != RT_TABLE_MAX)
>>> -
>>> struct in6_addr;
>>> struct hmap;
>>> struct vector;
>>> diff --git a/controller/route-exchange.c b/controller/route-exchange.c
>>> index b86eb43bf..879be9940 100644
>>> --- a/controller/route-exchange.c
>>> +++ b/controller/route-exchange.c
>>> @@ -34,6 +34,7 @@
>>> #include "route.h"
>>> #include "route-exchange.h"
>>> #include "route-exchange-netlink.h"
>>> +#include "netlink-utils.h"
>>>
>>> VLOG_DEFINE_THIS_MODULE(route_exchange);
>>> static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
>>> diff --git a/lib/ovn-util.c b/lib/ovn-util.c
>>> index d9b78fbb6..314575d00 100644
>>> --- a/lib/ovn-util.c
>>> +++ b/lib/ovn-util.c
>>> @@ -1897,3 +1897,160 @@ port_contains_duplicate_ip(struct lport_addresses
>>> *laddrs1,
>>>
>>> return false;
>>> }
>>> +
>>> +const struct in6_addr *
>>> +evpn_local_ip_lookup(const struct hmap *map, uint32_t vni)
>>> +{
>>> + struct vni_local_ip *e;
>>> + HMAP_FOR_EACH_WITH_HASH (e, hmap_node, hash_add(vni, 0), map) {
>>> + if (e->vni == vni) {
>>> + return &e->ip;
>>> + }
>>> + }
>>> + return NULL;
>>> +}
>>> +
>>> +ovs_be32
>>> +evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
>>> + uint32_t vni)
>>> +{
>>> + const struct in6_addr *addr =
>>> evpn_local_ip_lookup(&vni_ip_map->vni_ip4,
>>> + vni);
>>> + if (addr) {
>>> + return in6_addr_get_mapped_ipv4(addr);
>>> + }
>>> +
>>> + if (ipv6_addr_is_set(&vni_ip_map->default_ip4)) {
>>> + return in6_addr_get_mapped_ipv4(&vni_ip_map->default_ip4);
>>> + }
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +const struct in6_addr *
>>> +evpn_local_ip_find_v6(const struct evpn_local_ip_map *vni_ip_map,
>>> + uint32_t vni)
>>> +{
>>> + const struct in6_addr *addr =
>>> evpn_local_ip_lookup(&vni_ip_map->vni_ip6,
>>> + vni);
>>> + if (addr) {
>>> + return addr;
>>> + }
>>> +
>>> + if (ipv6_addr_is_set(&vni_ip_map->default_ip6)) {
>>> + return &vni_ip_map->default_ip6;
>>> + }
>>> +
>>> + return NULL;
>>> +}
>>> +
>>> +const struct in6_addr *
>>> +evpn_local_ip_map_lookup(const struct evpn_local_ip_map *map,
>>> + uint32_t vni, bool ipv4)
>>> +{
>>> + const struct hmap *hmap = ipv4 ? &map->vni_ip4 : &map->vni_ip6;
>>> + const struct in6_addr *addr = evpn_local_ip_lookup(hmap, vni);
>>> + if (addr) {
>>> + return addr;
>>> + }
>>> + const struct in6_addr *def = ipv4 ? &map->default_ip4 :
>>> &map->default_ip6;
>>> + return ipv6_addr_is_set(def) ? def : NULL;
>>> +}
>>> +
>>> +void
>>> +evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
>>> + const struct smap *config)
>>> +{
>>> + char *tokstr, *token, *ptr0 = NULL;
>>> +
>>> + const char *local_ip_str = smap_get_def(config, "ovn-evpn-local-ip",
>>> "");
>>> + tokstr = xstrdup(local_ip_str);
>>> + for (token = strtok_r(tokstr, ",", &ptr0); token;
>>> + token = strtok_r(NULL, ",", &ptr0)) {
>>> + char *ptr1 = NULL, *vni_str = strtok_r(token, "-", &ptr1);
>>> + char *ip_str = strtok_r(NULL, "-", &ptr1);
>>> + struct in6_addr ip;
>>> + uint32_t vni;
>>> +
>>> + if (ptr1 && *ptr1) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
>>> + VLOG_WARN_RL(&rl, "Malformed 'ovn-evpn-local-ip': %s", tokstr);
>>> + break;
>>> + }
>>> +
>>> + if (!ip_str) { /* default IP */
>>> + if (!ip46_parse(vni_str, &ip)) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> + VLOG_WARN_RL(&rl, "Invalid IP %s in 'ovn-evpn-local-ip'",
>>> + vni_str);
>>> + continue;
>>> + }
>>> + struct in6_addr *ip_ptr = IN6_IS_ADDR_V4MAPPED(&ip)
>>> + ? &vni_ip_map->default_ip4
>>> + : &vni_ip_map->default_ip6;
>>> + if (ipv6_addr_is_set(ip_ptr)) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> + VLOG_WARN_RL(&rl, "Default ovn-evpn-local-ip IP%d "
>>> + "already configured",
>>> + IN6_IS_ADDR_V4MAPPED(&ip) ? 4 : 6);
>>> + continue;
>>> + }
>>> + *ip_ptr = ip;
>>> + } else {
>>> + if (!ip46_parse(ip_str, &ip)) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> + VLOG_WARN_RL(&rl,
>>> + "EVPN enabled, but required
>>> 'ovn-evpn-local-ip' "
>>> + "is missing or invalid for vni %s", vni_str);
>>> + continue;
>>> + }
>>> +
>>> + if (!vni_str) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> + VLOG_WARN_RL(&rl, "Required 'ovn-evpn-local-ip' VNI not "
>>> + "configured for IP %s",
>>> + ip_str);
>>> + continue;
>>> + }
>>> +
>>> + if (!ovs_scan(vni_str, "%u", &vni) || !ovn_is_valid_vni(vni)) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> + VLOG_WARN_RL(&rl, "Invalid VNI %s in 'ovn-evpn-local-ip'",
>>> + vni_str);
>>> + continue;
>>> + }
>>> +
>>> + struct hmap *map = IN6_IS_ADDR_V4MAPPED(&ip)
>>> + ? &vni_ip_map->vni_ip4
>>> + : &vni_ip_map->vni_ip6;
>>> + if (evpn_local_ip_lookup(map, vni)) {
>>> + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5,
>>> 1);
>>> + VLOG_WARN_RL(&rl, "Duplicated VNI entry: %s", vni_str);
>>> + continue;
>>> + }
>>> +
>>> + struct vni_local_ip *e = xmalloc(sizeof *e);
>>> + *e = (struct vni_local_ip) {
>>> + .vni = vni,
>>> + .ip = ip,
>>> + };
>>> + hmap_insert(map, &e->hmap_node, hash_add(vni, 0));
>>> + }
>>> + }
>>> + free(tokstr);
>>> +}
>>> +
>>> +void
>>> +evpn_local_ip_map_destroy(struct evpn_local_ip_map *map)
>>> +{
>>> + struct vni_local_ip *e;
>>> + HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip4) {
>>> + free(e);
>>> + }
>>> + hmap_destroy(&map->vni_ip4);
>>> +
>>> + HMAP_FOR_EACH_POP (e, hmap_node, &map->vni_ip6) {
>>> + free(e);
>>> + }
>>> + hmap_destroy(&map->vni_ip6);
>>> +}
>>> diff --git a/lib/ovn-util.h b/lib/ovn-util.h
>>> index 29f6bb739..65aa53e75 100644
>>> --- a/lib/ovn-util.h
>>> +++ b/lib/ovn-util.h
>>> @@ -831,4 +831,29 @@ parse_neigh_dynamic_redistribute(const struct smap
>>> *options);
>>>
>>> size_t *shuffled_range(size_t n);
>>>
>>> +struct vni_local_ip {
>>> + struct hmap_node hmap_node;
>>> + struct in6_addr ip;
>>> + uint32_t vni;
>>> +};
>>> +
>>> +struct evpn_local_ip_map {
>>> + struct hmap vni_ip4; /* Per VNI local IPv4 vni_local_ips. */
>>> + struct hmap vni_ip6; /* Per VNI local IPv6 vni_local_ips. */
>>> + struct in6_addr default_ip4; /* Default local IPv4. */
>>> + struct in6_addr default_ip6; /* Default local IPv6. */
>>> +};
>>> +
>>> +const struct in6_addr *evpn_local_ip_lookup(const struct hmap *map,
>>> + uint32_t vni);
>>> +ovs_be32 evpn_local_ip_find_v4(const struct evpn_local_ip_map *vni_ip_map,
>>> + uint32_t vni);
>>> +const struct in6_addr *evpn_local_ip_find_v6(
>>> + const struct evpn_local_ip_map *vni_ip_map, uint32_t vni);
>>> +void evpn_local_ip_map_init(struct evpn_local_ip_map *vni_ip_map,
>>> + const struct smap *config);
>>> +const struct in6_addr *
>>> +evpn_local_ip_map_lookup(const struct evpn_local_ip_map *map,
>>> + uint32_t vni, bool ipv4);
>>> +void evpn_local_ip_map_destroy(struct evpn_local_ip_map *map);
>>> #endif /* OVN_UTIL_H */
>>> diff --git a/northd/en-datapath-logical-switch.c
>>> b/northd/en-datapath-logical-switch.c
>>> index ad6d042bc..a1ba217ba 100644
>>> --- a/northd/en-datapath-logical-switch.c
>>> +++ b/northd/en-datapath-logical-switch.c
>>> @@ -180,6 +180,20 @@ gather_external_ids(const struct nbrec_logical_switch
>>> *nbs,
>>> smap_add(external_ids, "dynamic-routing-arp-prefer-local",
>>> prefer_evpn_arp_local);
>>> }
>>> +
>>> + const char *ovn_maintain_evpn_mode =
>>> + smap_get(&nbs->other_config, "dynamic-routing-maintain-evpn");
>>> + if (ovn_maintain_evpn_mode) {
>>> + smap_add(external_ids, "dynamic-routing-maintain-evpn",
>>> + ovn_maintain_evpn_mode);
>>> + }
>>> +
>>> + const char *ovn_maintain_evpn_lrp =
>>> + smap_get(&nbs->other_config,
>>> "dynamic-routing-maintain-evpn-lrp");
>>> + if (ovn_maintain_evpn_lrp) {
>>> + smap_add(external_ids, "dynamic-routing-maintain-evpn-lrp",
>>> + ovn_maintain_evpn_lrp);
>>> + }
>>> }
>>>
>>> /* For backwards-compatibility, also store the NB UUID in
>>> diff --git a/ovn-nb.xml b/ovn-nb.xml
>>> index b0bb8c264..03d6536bb 100644
>>> --- a/ovn-nb.xml
>>> +++ b/ovn-nb.xml
>>> @@ -948,15 +948,30 @@
>>> </p>
>>>
>>> <p>
>>> - All of the following configuration options must also be provided
>>> - in order for the configuration to be valid:
>>> - <ref column="other_config" key="dynamic-routing-bridge-ifname"
>>> - table="Logical_switch"/>,
>>> - <ref column="other_config" key="dynamic-routing-vxlan-ifname"
>>> - table="Logical_switch"/>,
>>> - <ref column="other_config" key="dynamic-routing-advertise-ifname"
>>> - table="Logical_switch"/>.
>>> + The EVPN interfaces (VXLAN, bridge and advertise devices) can
>>> + either be named explicitly by the CMS, or auto-created and
>>> + maintained by <code>ovn-controller</code>:
>>> </p>
>>> +
>>> + <ul>
>>> + <li>
>>> + To name the interfaces explicitly, all of the following
>>> + configuration options must also be provided in order for the
>>> + configuration to be valid:
>>> + <ref column="other_config" key="dynamic-routing-bridge-ifname"
>>> + table="Logical_switch"/>,
>>> + <ref column="other_config" key="dynamic-routing-vxlan-ifname"
>>> + table="Logical_switch"/>,
>>> + <ref column="other_config"
>>> key="dynamic-routing-advertise-ifname"
>>> + table="Logical_switch"/>.
>>> + </li>
>>> + <li>
>>> + To have <code>ovn-controller</code> create and maintain the
>>> + interfaces automatically instead, set
>>> + <ref column="other_config" key="dynamic-routing-maintain-evpn"
>>> + table="Logical_switch"/>.
>>> + </li>
>>> + </ul>
>>> </column>
>>>
>>> <column name="other_config" key="dynamic-routing-bridge-ifname">
>>> @@ -979,6 +994,63 @@
>>> table="Logical_switch"/> is set to valid VNI.
>>> </column>
>>>
>>> + <column name="other_config" key="dynamic-routing-maintain-evpn"
>>> + type='{"type": "string", "enum": ["set", ["mvd"]]}'>
>>> + <p>
>>> + Enables automatic management of the EVPN interface stack by
>>> + <code>ovn-controller</code> for the VNI specified by
>>> + <ref column="other_config" key="dynamic-routing-vni"
>>> + table="Logical_switch"/>.
>>> + Instead of requiring the CMS to create and configure the
>>> interfaces,
>>> + <code>ovn-controller</code> creates and maintains them on every
>>> + chassis where the logical switch is local.
>>> + </p>
>>> +
>>> + <p>
>>> + The only supported value is <code>mvd</code> (VXLAN device per
>>> VNI).
>>> + </p>
>>> +
>>> + <p>
>>> + In this mode, <code>ovn-controller</code> creates the following
>>> + interfaces, each suffixed with the VNI:
>>> + <code>ovnbr<vni></code>,
>>> + <code>ovnvxlan<vni></code>, and
>>> + <code>ovnlo<vni></code>
>>> + (for example, <code>ovnvxlan100</code>).
>>> + </p>
>>> +
>>> + <p>
>>> + If <ref column="other_config" key="dynamic-routing-redistribute"
>>> + table="Logical_switch"/> includes <code>ip</code> and
>>> + <ref column="other_config"
>>> key="dynamic-routing-maintain-evpn-lrp"
>>> + table="Logical_switch"/> is configured,
>>> + <code>ovn-controller</code> also creates a VRF named
>>> + <code>ovnvrf<vni></code> and enslaves the bridge interface
>>> + to it. This allows the logical router IP addresses to be
>>> advertised
>>> + through EVPN.
>>> + </p>
>>> + </column>
>>> +
>>> + <column name="other_config" key="dynamic-routing-maintain-evpn-lrp">
>>> + <p>
>>> + Specifies the
>>> + <ref table="Logical_Router_Port"/> whose MAC address is assigned
>>> + to the auto-created bridge interface managed by
>>> + <ref column="other_config" key="dynamic-routing-maintain-evpn"
>>> + table="Logical_switch"/>.
>>> + This ensures that L3/IRB traffic terminating on the EVPN VNI uses
>>> + the logical router's MAC address.
>>> + </p>
>>> +
>>> + <p>
>>> + This option is required only when
>>> + <ref column="other_config" key="dynamic-routing-maintain-evpn"
>>> + table="Logical_switch"/> is enabled and
>>> + <ref column="other_config" key="dynamic-routing-redistribute"
>>> + table="Logical_switch"/> includes <code>ip</code>.
>>> + </p>
>>> + </column>
>>> +
>>> <column name="other_config" key="dynamic-routing-fdb-prefer-local"
>>> type='{"type": "boolean"}'>
>>> <p>
>>> diff --git a/tests/automake.mk b/tests/automake.mk
>>> index 8084357b5..1aa9f45c2 100644
>>> --- a/tests/automake.mk
>>> +++ b/tests/automake.mk
>>> @@ -291,6 +291,8 @@ tests_ovstest_SOURCES = \
>>>
>>> if HAVE_NETLINK
>>> tests_ovstest_SOURCES += \
>>> + controller/netlink-utils.h \
>>> + controller/netlink-utils.c \
>>> controller/host-if-monitor.c \
>>> controller/host-if-monitor.h \
>>> controller/neighbor-exchange-netlink.c \
>>> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
>>> index 17b4dcb3d..87e0e58a2 100644
>>> --- a/tests/system-ovn.at
>>> +++ b/tests/system-ovn.at
>>> @@ -21998,3 +21998,189 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port
>>> patch-.*/d
>>>
>>> AT_CLEANUP
>>> ])
>>> +
>>> +OVN_FOR_EACH_NORTHD([
>>> +AT_SETUP([dynamic-routing - EVPN maintain-evpn mvd])
>>> +AT_KEYWORDS([dynamic-routing evpn maintain-evpn])
>>> +
>>> +CHECK_VRF()
>>> +
>>> +vni=100
>>> +VRF_RESERVE([$vni])
>>> +
>>> +ovn_start
>>> +OVS_TRAFFIC_VSWITCHD_START()
>>> +ADD_BR([br-int])
>>> +
>>> +# Create a fake VXLAN interface, since there's no real peer chassis in
>>> +# this test to make OVS create vxlan_sys_<port> automatically.
>>> +ip link add vxlan_sys_4789 type vxlan dstport 4789 external
>>> +ip link set vxlan_sys_4789 up
>>> +
>>> +check ovs-vsctl \
>>> + -- set Open_vSwitch . external-ids:system-id=hv1 \
>>> + -- set Open_vSwitch .
>>> external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
>>> + -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
>>> + -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
>>> + -- set Open_vSwitch . external-ids:ovn-evpn-local-ip=169.0.0.1 \
>>> + -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
>>> +
>>> +start_daemon ovn-controller
>>> +ovs-vsctl add-port br-int p1 -- \
>>> + set Interface p1 external_ids:iface-id=ls-p1 -- \
>>> + set Interface p1 type=internal
>>> +check ovn-nbctl ls-add ls-evpn
>>> +check ovn-nbctl lsp-add ls-evpn ls-p1
>>> +check ovn-nbctl lsp-set-addresses ls-p1 "00:00:00:00:00:02 20.20.20.2"
>>> +
>>> +AS_BOX([Create EVPN interface stack - L2 VNI])
>>> +check ovn-nbctl \
>>> + -- set Logical_Switch ls-evpn \
>>> + other_config:dynamic-routing-vni=$vni \
>>> + other_config:dynamic-routing-maintain-evpn=mvd \
>>> + other_config:dynamic-routing-redistribute=fdb
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +# check no vrf exists since it's only fdb mode
>>> +AT_CHECK([ip a | grep -q ovnvrf100], [1])
>>> +AT_CHECK([ip a | grep -q ovnbr100], [0])
>>> +AT_CHECK([ip a | grep -q ovnvxlan100], [0])
>>> +AT_CHECK([ip a | grep -q ovnlo100], [0])
>>> +
>>> +dstport=$((60000 + $vni))
>>> +AT_CHECK([ip -d link show ovnvxlan$vni | grep -oP ' id \K[[0-9]]+'], [0],
>>> [dnl
>>> +100
>>> +], [ignore])
>>> +
>>> +AT_CHECK([ip -d link show ovnvxlan$vni | grep -oP 'dstport
>>> \K[[0-9]]+'],[0], [dnl
>>> +60100
>>> +], ignore)
>>> +
>>> +AT_CHECK([ip -d link show ovnvxlan$vni | grep -q 'local 169.0.0.1'], [0],
>>> [ignore], [ignore])
>>> +
>>> +AT_CHECK([ip -d link show ovnvxlan$vni | grep -q 'nolearning'], [0],
>>> [ignore], [ignore])
>>> +
>>> +AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'],[0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'],[0], [dnl
>>> +ovnbr100
>>> +])
>>> +
>>> +AS_BOX([Bridge MAC via dynamic-routing-evpn-lrp])
>>> +# Add LRP connected to ls-evpn; its MAC/IP will be applied to the bridge.
>>> +check ovn-nbctl \
>>> + -- lr-add R-evpn \
>>> + -- lrp-add R-evpn rp-evpn 00:00:00:aa:bb:01 192.168.100.1/24 \
>>> + -- lsp-add ls-evpn ls-evpn-rp \
>>> + -- set Logical_Switch_Port ls-evpn-rp type=router \
>>> + options:router-port=rp-evpn \
>>> + -- lsp-set-addresses ls-evpn-rp router \
>>> + -- set Logical_Switch ls-evpn \
>>> + other_config:dynamic-routing-maintain-evpn-lrp=rp-evpn \
>>> + other_config:dynamic-routing-redistribute=fdb,ip
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +# check vrf exists since it's fdb + ip mode
>>> +AT_CHECK([ip a | grep -q ovnvrf100], [0])
>>> +AT_CHECK([ip a | grep -q ovnbr100], [0])
>>> +AT_CHECK([ip a | grep -q ovnvxlan100], [0])
>>> +AT_CHECK([ip a | grep -q ovnlo100], [0])
>>> +
>>> +AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnvrf100
>>> +])
>>> +
>>> +AT_CHECK([ip link show ovnbr$vni | grep -oP 'link/ether \K\S+'], [0], [dnl
>>> +00:00:00:aa:bb:01
>>> +])
>>> +
>>> +AS_BOX([Bridge MAC address changes])
>>> +check ovn-nbctl set Logical_Router_Port rp-evpn \
>>> + mac='"00:00:00:00:00:08"'
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +AT_CHECK([ip link show ovnbr$vni | grep -oP 'link/ether \K\S+'],[0], [dnl
>>> +00:00:00:00:00:08
>>> +])
>>> +
>>> +AS_BOX([Redistribute mode change fdb,ip -> fdb: VRF destroyed, lo kept])
>>> +check ovn-nbctl set Logical_Switch ls-evpn \
>>> + other_config:dynamic-routing-redistribute=fdb
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +# check vrf exists since it's fdb + ip mode
>>> +AT_CHECK([ip a | grep -q ovnvrf100], [1])
>>> +AT_CHECK([ip a | grep -q ovnbr100], [0])
>>> +AT_CHECK([ip a | grep -q ovnvxlan100], [0])
>>> +AT_CHECK([ip a | grep -q ovnlo100], [0])
>>> +
>>> +AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnbr100
>>> +])
>>> +
>>> +AS_BOX([Redistribute mode change fdb -> ip: lo destroyed, VRF recreated])
>>> +check ovn-nbctl set Logical_Switch ls-evpn \
>>> + other_config:dynamic-routing-redistribute=ip
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +AT_CHECK([ip a | grep -q ovnvrf100], [0])
>>> +AT_CHECK([ip a | grep -q ovnbr100], [0])
>>> +AT_CHECK([ip a | grep -q ovnvxlan100], [0])
>>> +AT_CHECK([ip a | grep -q ovnlo100], [1])
>>> +AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'],[0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'],[0], [dnl
>>> +ovnvrf100
>>> +])
>>> +
>>> +AS_BOX([Redistribute mode change ip -> fdb,ip: lo and VRF both present
>>> again])
>>> +check ovn-nbctl set Logical_Switch ls-evpn \
>>> + other_config:dynamic-routing-redistribute=fdb,ip
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +AT_CHECK([ip a | grep -q ovnvrf100], [0])
>>> +AT_CHECK([ip a | grep -q ovnbr100], [0])
>>> +AT_CHECK([ip a | grep -q ovnvxlan100], [0])
>>> +AT_CHECK([ip a | grep -q ovnlo100], [0])
>>> +
>>> +AT_CHECK([ip link show ovnlo$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnvxlan$vni | grep -oP 'master \K\S+'], [0], [dnl
>>> +ovnbr100
>>> +])
>>> +AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'],[0], [dnl
>>> +ovnvrf100
>>> +])
>>> +
>>> +AS_BOX([Remove maintain-evpn - full stack cleanup])
>>> +check ovn-nbctl \
>>> + -- remove Logical_Switch ls-evpn other_config
>>> dynamic-routing-maintain-evpn
>>> +check ovn-nbctl --wait=hv sync
>>> +
>>> +AT_CHECK([ip a | grep -q ovnvrf100], [1])
>>> +AT_CHECK([ip a | grep -q ovnbr100], [1])
>>> +AT_CHECK([ip a | grep -q ovnvxlan100], [1])
>>> +AT_CHECK([ip a | grep -q ovnlo100], [1])
>>> +
>>> +ip link del vxlan_sys_4789
>>> +
>>> +OVN_CLEANUP_CONTROLLER([hv1])
>>> +OVN_CLEANUP_NORTHD
>>> +as
>>> +OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
>>> +/connection dropped.*/d
>>> +/misses dynamic-routing.*/d"])
>>> +AT_CLEANUP
>>> +])
>
>
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev