Until now the CMS had to create the EVPN interface stack (bridge, VXLAN and advertise devices) on every chassis itself and pass the interface names via the "dynamic-routing-bridge-ifname", "dynamic-routing-vxlan-ifname" and "dynamic-routing-advertise-ifname" options of the Logical_Switch.
Add a new "dynamic-routing-maintain-evpn" option, which makes ovn-controller create and maintain those interfaces on its own on every chassis where the logical switch is local. The only supported value is "mvd", i.e. one VXLAN device per VNI. The devices are named after the VNI: ovnbr<vni>, ovnvxlan<vni> and ovnlo<vni>. If "dynamic-routing-redistribute" includes "ip", the logical router port has to be specified with "dynamic-routing-maintain-evpn-lrp". In that case ovn-controller also creates the ovnvrf<vni> VRF, enslaves the bridge to it and assigns the router port MAC to the bridge, so that the logical router addresses can be advertised over EVPN. The interfaces are removed on graceful shutdown of ovn-controller. The UDP destination port of the VXLAN device is configurable with the "dynamic-routing-maintain-evpn-vxlan-port" option and defaults to 4790. It has to differ from the port used by the OVS datapath VXLAN device, because the maintained device only carries EVPN state for the routing daemon. Signed-off-by: Alexandra Rukomoinikova <[email protected]> --- controller/host-if-monitor.c | 15 ++ controller/host-if-monitor.h | 2 +- controller/neighbor-exchange-stub.c | 10 + controller/neighbor-exchange.c | 350 +++++++++++++++++++++++++++- controller/neighbor-exchange.h | 8 + controller/neighbor.c | 205 +++++++++++++--- controller/neighbor.h | 37 +++ controller/netlink-utils.h | 4 +- controller/ovn-controller.c | 20 ++ lib/ovn-util.c | 24 ++ lib/ovn-util.h | 2 +- northd/en-datapath-logical-switch.c | 22 ++ ovn-nb.xml | 113 ++++++++- tests/system-ovn.at | 325 ++++++++++++++++++++++++++ 14 files changed, 1094 insertions(+), 43 deletions(-) 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-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..e21eaf8f0 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,353 @@ static int neighbor_exchange_nl_status; } \ } while (0) +/* Tracks the EVPN interface stack of a single VNI. + * + * Presence in 'maintained_evpn_entries' means "OVN may have created kernel + * devices for this VNI and is therefore responsible for tearing them down". + * An entry is inserted before the first netlink call is issued, so that a + * stack that was only partially created is still cleaned up later. */ +struct maintained_evpn_entry { + struct hmap_node node; + /* The configuration the devices were last (re)created with, i.e. what + * the kernel is expected to hold. It is updated as soon as the + * previous devices are gone, even if creating the new ones then fails, + * so that a partial failure doesn't leave a permanent difference + * against the desired configuration. */ + struct neighbor_ovn_maintain_entry entry; + /* False if the last attempt to apply 'entry' didn't fully succeed. The + * next run then repairs the stack by re-running the idempotent create + * path instead of tearing it down and rebuilding it. */ + bool synced; +}; + +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_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 const 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)); +} + +/* Brings the kernel devices of a VNI in line with the desired configuration + * 'entry', recording in 'me' what was actually applied. 'has_old' tells + * whether 'me->entry' describes devices we created in a previous run. */ +static void +maintain_evpn_devices(const struct neighbor_ovn_maintain_entry *entry, + struct maintained_evpn_entry *me, bool has_old) +{ + if (has_old && should_destroy_interfaces(entry, &me->entry)) { + if (!evpn_delete_devices(&me->entry)) { + /* The old devices are (partly) still around, so 'me->entry' + * keeps describing them and the next run retries the + * teardown. */ + me->synced = false; + return; + } + } else if (me->synced) { + /* Nothing changed and the stack was fully applied. */ + return; + } + + /* Either there were no devices yet, or the old ones are now gone: from + * here on 'entry' is what the kernel is expected to hold, even if the + * creation below only partially succeeds. Otherwise the difference + * against 'entry' would never go away and we would tear the stack down + * and rebuild it on every single run. */ + me->entry = *entry; + me->synced = evpn_create_devices(&me->entry); +} + +static void +neighbor_exchange_maintain_evpn_run(const struct vector *maintain_evpn) +{ + 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 *me = + maintained_evpn_entry_find(&old_maintained_evpn_entries, + entry->vni); + bool has_old = me != NULL; + + if (has_old) { + hmap_remove(&old_maintained_evpn_entries, &me->node); + } else { + me = xzalloc(sizeof *me); + } + + /* Start tracking the VNI before issuing any netlink call to be + * responsible for deleting/updating its devices */ + hmap_insert(&maintained_evpn_entries, &me->node, + hash_int(entry->vni, 0)); + + maintain_evpn_devices(entry, me, has_old); + } + + struct maintained_evpn_entry *stale_me; + HMAP_FOR_EACH_POP (stale_me, node, &old_maintained_evpn_entries) { + if (evpn_delete_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'). + * Some devices may already be gone, so if the VNI comes back + * before the teardown completes, the stack has to be repaired + * rather than assumed to be in shape. */ + stale_me->synced = false; + 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_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) { + /* Reset once for the whole run: errors hit while maintaining the EVPN + * devices must survive into neighbor_exchange_status_run(), otherwise + * the engine is never woken up to retry a failed or partial apply. */ + CLEAR_NEIGHBOR_EXCHANGE_NL_STATUS(); + + neighbor_exchange_maintain_evpn_run(n_ctx_in->maintain_evpn); + struct neighbor_interface_monitor *nim; struct sset if_names = SSET_INITIALIZER(&if_names); @@ -78,7 +427,6 @@ neighbor_exchange_run(const struct neighbor_exchange_ctx_in *n_ctx_in, host_if_monitor_update_watches(&if_names); sset_destroy(&if_names); - CLEAR_NEIGHBOR_EXCHANGE_NL_STATUS(); VECTOR_FOR_EACH (n_ctx_in->monitored_interfaces, nim) { int32_t if_index = host_if_monitor_ifname_toindex(nim->if_name); 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 c06183063..cb4d5fda2 100644 --- a/controller/neighbor.c +++ b/controller/neighbor.c @@ -18,6 +18,7 @@ #include "lib/hash.h" #include "lib/packets.h" #include "lib/sset.h" +#include "lib/ovn-util.h" #include "local_data.h" #include "lport.h" #include "openvswitch/ofp-parse.h" @@ -27,6 +28,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", @@ -91,13 +93,133 @@ 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) +{ + long long int vxlan_port = + ovn_smap_get_llong(&ld->datapath->external_ids, + "dynamic-routing-maintain-evpn-vxlan-port", + EVPN_DEFAULT_FAKE_VXLAN_PORT); + if (vxlan_port <= 0 || vxlan_port > UINT16_MAX) { + VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn-vxlan-port " + "%lld for datapath "UUID_FMT" is not a valid UDP port", + vxlan_port, UUID_ARGS(&ld->datapath->header_.uuid)); + return false; + } + + *entry = (struct neighbor_ovn_maintain_entry) { + .redistribute_mode = redistribute_mode, + .mainatain_mode = mainatain_mode, + .vni = vni, + .fake_vxlan_port = vxlan_port, + }; + + 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 = + evpn_local_ip_map_lookup(n_ctx_in->evpn_local_ip_map, vni); + + 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; + } + + /* The option may either name a logical router port or directly + * specify the MAC address to be used on the bridge interface. The + * latter is useful when the router port has multiple MAC addresses + * and the CMS needs to pick a specific one. */ + struct eth_addr mac; + if (eth_addr_from_string(lrp_name, &mac)) { + entry->br_config.has_addr = true; + entry->br_config.lladdr = mac; + return true; + } + + 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-maintain-evpn-lrp: port \"%s\" " + "not found", lrp_name); + return false; + } + + const struct sbrec_port_binding *peer = + lport_get_peer(pb, n_ctx_in->sbrec_pb_by_name); + if (!peer) { + VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn-lrp: port \"%s\" " + "is not a router port", lrp_name); + return false; + } + if (peer->datapath != ld->datapath) { + VLOG_WARN_RL(&rl, "dynamic-routing-maintain-evpn-lrp: router port " + "\"%s\" is not attached to datapath "UUID_FMT, + lrp_name, UUID_ARGS(&ld->datapath->header_.uuid)); + 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) @@ -119,24 +241,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)) { @@ -149,32 +297,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, @@ -189,7 +336,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 cd158a0f1..fd19367cc 100644 --- a/controller/neighbor.h +++ b/controller/neighbor.h @@ -24,6 +24,7 @@ #include "hmapx.h" #include "lib/sset.h" #include "openvswitch/hmap.h" +#include "lib/ovn-util.h" #include "vec.h" @@ -54,6 +55,7 @@ struct neighbor_ctx_in { /* Index for FDB by dp_key. */ struct ovsdb_idl_index *sbrec_fdb_by_dp_key; const struct sbrec_chassis *chassis; + struct evpn_local_ip_map *evpn_local_ip_map; }; struct neighbor_ctx_out { @@ -64,6 +66,9 @@ struct neighbor_ctx_out { /* Contains 'struct local_datapath' pointers for datapaths with FDB * advertisement enabled. */ struct hmapx *fdb_datapaths; + /* 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; }; enum neighbor_interface_type { @@ -72,6 +77,38 @@ enum neighbor_interface_type { NEIGH_IFACE_LOOPBACK, }; +enum neighbor_mainatain_mode { + NEIGH_MAINTAIN_NONE, + NEIGH_MAINTAIN_MVD, +}; + +/* UDP destination port used for the OVN maintained VXLAN devices if + * "dynamic-routing-maintain-evpn-vxlan-port" is not set. It must differ + * from the port used by the OVS datapath VXLAN device (4789), because + * those devices are only used to exchange EVPN state with the routing + * daemon and never carry traffic. */ +#define EVPN_DEFAULT_FAKE_VXLAN_PORT 4790 + +/* 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 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; + uint16_t fake_vxlan_port; +}; + struct neighbor_interface_monitor { enum neighbor_family family; char if_name[IFNAMSIZ + 1]; diff --git a/controller/netlink-utils.h b/controller/netlink-utils.h index 51e4ebb08..a34c9db93 100644 --- a/controller/netlink-utils.h +++ b/controller/netlink-utils.h @@ -1,6 +1,4 @@ -/* - * Copyright (c) 2025 Canonical, Ltd. - * Copyright (c) 2025, STACKIT GmbH & Co. KG +/* 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. diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c index be02d2864..a0861f641 100644 --- a/controller/ovn-controller.c +++ b/controller/ovn-controller.c @@ -44,6 +44,7 @@ #include "lib/lflow-conj-ids.h" #include "lib/vswitch-idl.h" #include "lib/ovsdb-types.h" +#include "lib/ovn-util.h" #include "local_data.h" #include "lport.h" #include "memory.h" @@ -6164,6 +6165,9 @@ struct ed_type_neighbor { /* Contains 'struct local_datapath' pointers for datapaths with FDB * advertisement enabled. */ struct hmapx fdb_datapaths; + /* Contains struct neighbor_ovn_maintain_entry, one per VNI + * whose EVPN interfaces are auto-created and maintained. */ + struct vector maintain_evpn; }; static void * @@ -6177,6 +6181,8 @@ en_neighbor_init(struct engine_node *node OVS_UNUSED, VECTOR_EMPTY_INITIALIZER(struct neighbor_interface_monitor *), .advertised_pbs = SSET_INITIALIZER(&data->advertised_pbs), .fdb_datapaths = HMAPX_INITIALIZER(&data->fdb_datapaths), + .maintain_evpn = + VECTOR_EMPTY_INITIALIZER(struct neighbor_ovn_maintain_entry), }; return data; } @@ -6188,6 +6194,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); hmapx_destroy(&ne_data->fdb_datapaths); } @@ -6231,6 +6238,12 @@ 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, @@ -6239,18 +6252,22 @@ en_neighbor_run(struct engine_node *node OVS_UNUSED, void *data) .sbrec_pb_by_key = sbrec_port_binding_by_key, .sbrec_fdb_by_dp_key = sbrec_fdb_by_dp_key, .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, .fdb_datapaths = &ne_data->fdb_datapaths, + .maintain_evpn = &ne_data->maintain_evpn, }; neighbor_cleanup(&ne_data->monitored_interfaces); sset_clear(&ne_data->advertised_pbs); hmapx_clear(&ne_data->fdb_datapaths); + 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; } @@ -6612,6 +6629,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, @@ -8798,6 +8816,7 @@ loop_done: poll_block(); } route_exchange_cleanup_vrfs(); + neighbor_exchange_maintain_evpn_cleanup_all(); } /* The engine cleanup should happen only after threads have been @@ -8836,6 +8855,7 @@ loop_done: dns_resolve_destroy(); route_exchange_destroy(); ovn_netlink_notifiers_destroy(); + neighbor_exchange_maintain_evpn_destroy(); exit(retval); } diff --git a/lib/ovn-util.c b/lib/ovn-util.c index 6e9d1150e..f36fefc03 100644 --- a/lib/ovn-util.c +++ b/lib/ovn-util.c @@ -1898,6 +1898,30 @@ port_contains_duplicate_ip(struct lport_addresses *laddrs1, return false; } +const struct in6_addr * +evpn_local_ip_map_lookup(const struct evpn_local_ip_map *map, uint32_t vni) +{ + const struct in6_addr *addr = evpn_local_ip_lookup(&map->vni_ip4, vni); + if (addr) { + return addr; + } + + addr = evpn_local_ip_lookup(&map->vni_ip6, vni); + if (addr) { + return addr; + } + + if (ipv6_addr_is_set(&map->default_ip4)) { + return &map->default_ip4; + } + + if (ipv6_addr_is_set(&map->default_ip6)) { + return &map->default_ip6; + } + + return NULL; +} + const struct in6_addr * evpn_local_ip_lookup(const struct hmap *map, uint32_t vni) { diff --git a/lib/ovn-util.h b/lib/ovn-util.h index 8364ede5c..8fb28a9d4 100644 --- a/lib/ovn-util.h +++ b/lib/ovn-util.h @@ -861,7 +861,7 @@ 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); + uint32_t vni); 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 874a608a7..528cccb96 100644 --- a/northd/en-datapath-logical-switch.c +++ b/northd/en-datapath-logical-switch.c @@ -173,6 +173,28 @@ gather_external_ids(const struct nbrec_logical_switch *nbs, smap_add(external_ids, "dynamic-routing-redistribute", redistribute); } + + 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); + } + + const char *ovn_maintain_evpn_vxlan_port = + smap_get(&nbs->other_config, + "dynamic-routing-maintain-evpn-vxlan-port"); + if (ovn_maintain_evpn_vxlan_port) { + smap_add(external_ids, "dynamic-routing-maintain-evpn-vxlan-port", + ovn_maintain_evpn_vxlan_port); + } } /* For backwards-compatibility, also store the NB UUID in diff --git a/ovn-nb.xml b/ovn-nb.xml index c741a3b32..81c9e7ee7 100644 --- a/ovn-nb.xml +++ b/ovn-nb.xml @@ -967,15 +967,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"> @@ -998,6 +1013,88 @@ 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> + Alternatively, an Ethernet address (for example, + <code>00:00:00:00:01:00</code>) may be specified directly instead + of a port name; it is then used as is for the bridge interface. + This is useful when the router port is configured with multiple MAC + addresses and the CMS needs to select a specific one. + </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-maintain-evpn-vxlan-port" + type='{"type": "integer", "minInteger": 1, + "maxInteger": 65535}'> + <p> + The UDP destination port configured on the VXLAN device created by + <ref column="other_config" key="dynamic-routing-maintain-evpn" + table="Logical_switch"/>. Defaults to <code>4790</code>. + </p> + + <p> + This device is only used to exchange EVPN state with the routing + daemon, it never carries traffic, so the port must differ from the + one used by the OVS datapath VXLAN device (<code>4789</code>). + </p> + </column> + <column name="other_config" key="dynamic-routing-fdb-prefer-local" type='{"type": "boolean"}'> <p> diff --git a/tests/system-ovn.at b/tests/system-ovn.at index 973c46728..5f619c17e 100644 --- a/tests/system-ovn.at +++ b/tests/system-ovn.at @@ -23741,3 +23741,328 @@ 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 configured +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 -d link show ovnvxlan$vni | grep -oP ' id \K[[0-9]]+'], [0], [dnl +100 +], [ignore]) + +# The default dstport is used since it is not requested explicitly. +AT_CHECK([ip -d link show ovnvxlan$vni | grep -oP 'dstport \K[[0-9]]+'],[0], [dnl +4790 +], ignore) + +# Request a different dstport for the VXLAN device. +check ovn-nbctl \ + -- set Logical_Switch ls-evpn \ + other_config:dynamic-routing-maintain-evpn-vxlan-port=4791 +check ovn-nbctl --wait=hv sync + +OVS_WAIT_UNTIL([test "$(ip -d link show ovnvxlan$vni | + grep -oP 'dstport \K[[0-9]]+')" = "4791"]) + +check ovn-nbctl \ + -- remove Logical_Switch ls-evpn other_config \ + dynamic-routing-maintain-evpn-vxlan-port +check ovn-nbctl --wait=hv sync + +OVS_WAIT_UNTIL([test "$(ip -d link show ovnvxlan$vni | + grep -oP 'dstport \K[[0-9]]+')" = "4790"]) + +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([dynamic-routing-maintain-evpn-lrp must name a router port of this LS]) +# The MAC of that port is advertised as the EVPN next-hop for this VNI, so a +# port that is not a router port must be rejected. +check ovn-nbctl set Logical_Switch ls-evpn \ + other_config:dynamic-routing-maintain-evpn-lrp=ls-p1 +check ovn-nbctl --wait=hv sync +OVS_WAIT_UNTIL([grep -q 'is not a router port' ovn-controller.log]) +AT_CHECK([ip link show ovnbr$vni], [1], [ignore], [ignore]) + +# A router port of a different Logical Switch must be rejected as well: its +# MAC does not belong to this L2 domain. ls-other needs a port bound to +# this chassis so that its port bindings are actually monitored here. +ovs-vsctl add-port br-int p2 -- \ + set Interface p2 external_ids:iface-id=ls-other-p1 -- \ + set Interface p2 type=internal +check ovn-nbctl \ + -- ls-add ls-other \ + -- lsp-add ls-other ls-other-p1 \ + -- lsp-set-addresses ls-other-p1 "00:00:00:00:00:03 20.20.30.3" \ + -- lr-add R-other \ + -- lrp-add R-other rp-other 00:00:00:aa:bb:02 192.168.200.1/24 \ + -- lsp-add ls-other ls-other-rp \ + -- set Logical_Switch_Port ls-other-rp type=router \ + options:router-port=rp-other \ + -- lsp-set-addresses ls-other-rp router \ + -- set Logical_Switch ls-evpn \ + other_config:dynamic-routing-maintain-evpn-lrp=rp-other +check ovn-nbctl --wait=hv sync +OVS_WAIT_UNTIL([grep -q 'is not attached to datapath' ovn-controller.log]) +AT_CHECK([ip link show ovnbr$vni], [1], [ignore], [ignore]) + +# Back to the router port of ls-evpn: the stack must come back. +check ovn-nbctl set Logical_Switch ls-evpn \ + other_config:dynamic-routing-maintain-evpn-lrp=rp-evpn +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([dynamic-routing-maintain-evpn-lrp accepts a MAC address]) +# Instead of a port name, the option may carry an Ethernet address, which is +# then used as is. +check ovn-nbctl set Logical_Switch ls-evpn \ + other_config:dynamic-routing-maintain-evpn-lrp='"00:00:00:aa:bb:09"' +check ovn-nbctl --wait=hv sync + +OVS_WAIT_UNTIL([test "$(ip link show ovnbr$vni | + grep -oP 'link/ether \K\S+')" = "00:00:00:aa:bb:09"]) +AT_CHECK([ip a | grep -q ovnvrf100], [0]) +AT_CHECK([ip a | grep -q ovnvxlan100], [0]) +AT_CHECK([ip a | grep -q ovnlo100], [0]) +AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'], [0], [dnl +ovnvrf100 +]) + +# Back to the port name: the MAC of the router port is used again. +check ovn-nbctl set Logical_Switch ls-evpn \ + other_config:dynamic-routing-maintain-evpn-lrp=rp-evpn +check ovn-nbctl set Logical_Router_Port rp-evpn mac='"00:00:00:00:00:08"' +check ovn-nbctl --wait=hv sync + +OVS_WAIT_UNTIL([test "$(ip link show ovnbr$vni | + grep -oP 'link/ether \K\S+')" = "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([ovn-controller exit --restart: EVPN stack must survive]) +br_ifindex=$(cat /sys/class/net/ovnbr$vni/ifindex) +vxlan_ifindex=$(cat /sys/class/net/ovnvxlan$vni/ifindex) +lo_ifindex=$(cat /sys/class/net/ovnlo$vni/ifindex) +vrf_ifindex=$(cat /sys/class/net/ovnvrf$vni/ifindex) + +TMPPID=$(cat $OVS_RUNDIR/ovn-controller.pid) +check ovs-appctl -t ovn-controller exit --restart +OVS_WAIT_WHILE([kill -0 $TMPPID 2>/dev/null]) + +AT_CHECK([test "$(cat /sys/class/net/ovnvrf$vni/ifindex)" = "$vrf_ifindex"]) +AT_CHECK([test "$(cat /sys/class/net/ovnbr$vni/ifindex)" = "$br_ifindex"]) +AT_CHECK([test "$(cat /sys/class/net/ovnvxlan$vni/ifindex)" = "$vxlan_ifindex"]) +AT_CHECK([test "$(cat /sys/class/net/ovnlo$vni/ifindex)" = "$lo_ifindex"]) + +# Starting again must adopt the existing stack instead of recreating it: +# the ifindexes have to stay the same and the topology must be intact. +start_daemon ovn-controller +check ovn-nbctl --wait=hv sync + +AT_CHECK([test "$(cat /sys/class/net/ovnvrf$vni/ifindex)" = "$vrf_ifindex"]) +AT_CHECK([test "$(cat /sys/class/net/ovnbr$vni/ifindex)" = "$br_ifindex"]) +AT_CHECK([test "$(cat /sys/class/net/ovnvxlan$vni/ifindex)" = "$vxlan_ifindex"]) +AT_CHECK([test "$(cat /sys/class/net/ovnlo$vni/ifindex)" = "$lo_ifindex"]) + +AT_CHECK([ip link show ovnbr$vni | grep -oP 'master \K\S+'], [0], [dnl +ovnvrf100 +]) +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 +]) +AT_CHECK([ip link show ovnbr$vni | grep -oP 'link/ether \K\S+'], [0], [dnl +00:00:00:00:00:08 +]) + +AS_BOX([Graceful ovn-controller exit: EVPN stack must be cleaned up]) +OVS_APP_EXIT_AND_WAIT([ovn-controller]) + +AT_CHECK([ip link show ovnvrf$vni], [1], [ignore], [ignore]) +AT_CHECK([ip link show ovnbr$vni], [1], [ignore], [ignore]) +AT_CHECK([ip link show ovnvxlan$vni], [1], [ignore], [ignore]) +AT_CHECK([ip link show ovnlo$vni], [1], [ignore], [ignore]) + +# Bring the stack back for the remaining checks. +start_daemon ovn-controller +OVS_WAIT_UNTIL([test "$(ovn-sbctl --bare --columns=name find chassis \ + name=hv1)" = "hv1"]) +check ovn-nbctl --wait=hv sync +AT_CHECK([ip link show ovnvrf$vni], [0], [ignore], [ignore]) +AT_CHECK([ip link show ovnbr$vni], [0], [ignore], [ignore]) +AT_CHECK([ip link show ovnvxlan$vni], [0], [ignore], [ignore]) +AT_CHECK([ip link show ovnlo$vni], [0], [ignore], [ignore]) + +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 +/dynamic-routing-maintain-evpn-lrp.*/d"]) +AT_CLEANUP +]) -- 2.48.1 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
