|
| 1 | +/* Copyright (c) 2026, Red Hat, Inc. |
| 2 | + * |
| 3 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | + * you may not use this file except in compliance with the License. |
| 5 | + * You may obtain a copy of the License at: |
| 6 | + * |
| 7 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | + * |
| 9 | + * Unless required by applicable law or agreed to in writing, software |
| 10 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | + * See the License for the specific language governing permissions and |
| 13 | + * limitations under the License. |
| 14 | + */ |
| 15 | + |
| 16 | +#include <config.h> |
| 17 | + |
| 18 | +#include "openvswitch/poll-loop.h" |
| 19 | +#include "packets.h" |
| 20 | +#include "timeval.h" |
| 21 | + |
| 22 | +#include "evpn-arp.h" |
| 23 | +#include "evpn-mac-binding-sync.h" |
| 24 | +#include "lib/mac-binding-index.h" |
| 25 | +#include "local_data.h" |
| 26 | +#include "mac-cache.h" |
| 27 | +#include "ovn-sb-idl.h" |
| 28 | +#include "ovn-util.h" |
| 29 | +#include "vec.h" |
| 30 | + |
| 31 | +/* Hash a (logical_port, ip) pair. */ |
| 32 | +static uint32_t |
| 33 | +evpn_mb_hash(const char *logical_port, const char *ip) |
| 34 | +{ |
| 35 | + uint32_t hash = hash_string(logical_port, 0); |
| 36 | + return hash_string(ip, hash); |
| 37 | +} |
| 38 | + |
| 39 | +static struct evpn_mb_synced_entry * |
| 40 | +evpn_mb_synced_find(const struct hmap *synced, const char *logical_port, |
| 41 | + const char *ip) |
| 42 | +{ |
| 43 | + uint32_t hash = evpn_mb_hash(logical_port, ip); |
| 44 | + |
| 45 | + struct evpn_mb_synced_entry *entry; |
| 46 | + HMAP_FOR_EACH_WITH_HASH (entry, hmap_node, hash, synced) { |
| 47 | + if (!strcmp(entry->logical_port, logical_port) && |
| 48 | + !strcmp(entry->ip, ip)) { |
| 49 | + return entry; |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + return NULL; |
| 54 | +} |
| 55 | + |
| 56 | +static struct evpn_mb_synced_entry * |
| 57 | +evpn_mb_synced_add(struct hmap *synced, const char *logical_port, |
| 58 | + const char *ip) |
| 59 | +{ |
| 60 | + struct evpn_mb_synced_entry *entry = xmalloc(sizeof *entry); |
| 61 | + *entry = (struct evpn_mb_synced_entry) { |
| 62 | + .logical_port = xstrdup(logical_port), |
| 63 | + .ip = xstrdup(ip), |
| 64 | + }; |
| 65 | + hmap_insert(synced, &entry->hmap_node, evpn_mb_hash(logical_port, ip)); |
| 66 | + return entry; |
| 67 | +} |
| 68 | + |
| 69 | +static void |
| 70 | +evpn_mb_synced_remove(struct hmap *synced, struct evpn_mb_synced_entry *entry) |
| 71 | +{ |
| 72 | + hmap_remove(synced, &entry->hmap_node); |
| 73 | + free(entry->logical_port); |
| 74 | + free(entry->ip); |
| 75 | + free(entry); |
| 76 | +} |
| 77 | + |
| 78 | +/* Schedule the waker to fire after 'delay_ms' milliseconds. */ |
| 79 | +static void |
| 80 | +evpn_mb_sync_waker_schedule(struct evpn_mb_sync_waker *waker, |
| 81 | + int64_t delay_ms) |
| 82 | +{ |
| 83 | + if (delay_ms < INT64_MAX) { |
| 84 | + waker->should_schedule = true; |
| 85 | + waker->next_wake_msec = time_msec() + delay_ms; |
| 86 | + poll_timer_wait_until(waker->next_wake_msec); |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +/* Sync a single EVPN ARP entry to the SB MAC_Binding table for one |
| 91 | + * router port. Caller must ensure 'ovnsb_idl_txn' is valid. |
| 92 | + * Returns the remaining time (in ms) until the next timestamp |
| 93 | + * refresh is needed, or INT64_MAX if none. */ |
| 94 | +static int64_t |
| 95 | +sync_evpn_mb_for_router_port( |
| 96 | + struct ovsdb_idl_txn *ovnsb_idl_txn, |
| 97 | + struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip, |
| 98 | + const struct sbrec_mac_binding_table *mb_table, |
| 99 | + const struct sbrec_port_binding *router_pb, |
| 100 | + const struct evpn_arp *arp, |
| 101 | + struct mac_cache_data *mac_cache_data, |
| 102 | + struct ed_type_evpn_mac_binding_sync *data, |
| 103 | + long long timewall_now) |
| 104 | +{ |
| 105 | + char *ip_s = normalize_v46(&arp->ip); |
| 106 | + struct evpn_mb_synced_entry *existing = |
| 107 | + evpn_mb_synced_find(&data->synced_entries, |
| 108 | + router_pb->logical_port, ip_s); |
| 109 | + |
| 110 | + const struct sbrec_mac_binding *sb_mb = existing |
| 111 | + ? sbrec_mac_binding_table_get_for_uuid(mb_table, &existing->mb_uuid) |
| 112 | + : NULL; |
| 113 | + |
| 114 | + /* Insert or update the SB MAC_Binding row. */ |
| 115 | + const struct sbrec_mac_binding *b = |
| 116 | + mac_binding_add_to_sb(ovnsb_idl_txn, |
| 117 | + sbrec_mac_binding_by_lport_ip, |
| 118 | + router_pb->logical_port, |
| 119 | + router_pb->datapath, |
| 120 | + arp->mac, ip_s, false, |
| 121 | + sb_mb); |
| 122 | + |
| 123 | + if (!existing) { |
| 124 | + existing = evpn_mb_synced_add(&data->synced_entries, |
| 125 | + router_pb->logical_port, ip_s); |
| 126 | + } |
| 127 | + |
| 128 | + free(ip_s); |
| 129 | + existing->mb_uuid = b->header_.uuid; |
| 130 | + existing->stale = false; |
| 131 | + |
| 132 | + /* Refresh timestamp to prevent aging. */ |
| 133 | + struct mac_cache_threshold *threshold = |
| 134 | + mac_cache_threshold_find(mac_cache_data, |
| 135 | + router_pb->datapath->tunnel_key); |
| 136 | + if (!threshold) { |
| 137 | + return INT64_MAX; |
| 138 | + } |
| 139 | + |
| 140 | + uint64_t since_updated = timewall_now - b->timestamp; |
| 141 | + if (since_updated >= threshold->cooldown_period) { |
| 142 | + sbrec_mac_binding_set_timestamp(b, timewall_now); |
| 143 | + return threshold->cooldown_period; |
| 144 | + } |
| 145 | + |
| 146 | + return threshold->cooldown_period - since_updated; |
| 147 | +} |
| 148 | + |
| 149 | +void |
| 150 | +evpn_mac_binding_sync_run( |
| 151 | + struct ovsdb_idl_txn *ovnsb_idl_txn, |
| 152 | + struct ovsdb_idl_index *sbrec_mac_binding_by_lport_ip, |
| 153 | + const struct sbrec_mac_binding_table *mb_table, |
| 154 | + const struct hmap *local_datapaths, |
| 155 | + const struct hmap *evpn_arps, |
| 156 | + struct mac_cache_data *mac_cache_data, |
| 157 | + struct ed_type_evpn_mac_binding_sync *data, |
| 158 | + struct evpn_mb_sync_waker *waker) |
| 159 | +{ |
| 160 | + if (!ovnsb_idl_txn) { |
| 161 | + data->sb_changes_pending = true; |
| 162 | + return; |
| 163 | + } |
| 164 | + |
| 165 | + long long timewall_now = time_wall_msec(); |
| 166 | + int64_t min_next_refresh_ms = INT64_MAX; |
| 167 | + |
| 168 | + /* Mark all synced entries as stale. SB row pointers are resolved |
| 169 | + * lazily by UUID only where needed (insert/update, timestamp |
| 170 | + * refresh, delete), avoiding a per-entry index lookup here. */ |
| 171 | + struct evpn_mb_synced_entry *synced_entry; |
| 172 | + HMAP_FOR_EACH (synced_entry, hmap_node, &data->synced_entries) { |
| 173 | + synced_entry->stale = true; |
| 174 | + } |
| 175 | + |
| 176 | + /* Walk current EVPN ARPs and sync to SB. */ |
| 177 | + const struct evpn_arp *arp; |
| 178 | + HMAP_FOR_EACH (arp, hmap_node, evpn_arps) { |
| 179 | + const struct peer_ports *peers; |
| 180 | + VECTOR_FOR_EACH_PTR (&arp->ldp->peer_ports, peers) { |
| 181 | + const struct sbrec_port_binding *remote_pb = peers->remote; |
| 182 | + struct local_datapath *peer_ld = |
| 183 | + get_local_datapath(local_datapaths, |
| 184 | + remote_pb->datapath->tunnel_key); |
| 185 | + if (!peer_ld || peer_ld->is_switch) { |
| 186 | + continue; |
| 187 | + } |
| 188 | + |
| 189 | + int64_t remaining = |
| 190 | + sync_evpn_mb_for_router_port(ovnsb_idl_txn, |
| 191 | + sbrec_mac_binding_by_lport_ip, |
| 192 | + mb_table, |
| 193 | + remote_pb, arp, |
| 194 | + mac_cache_data, data, |
| 195 | + timewall_now); |
| 196 | + if (remaining < min_next_refresh_ms) { |
| 197 | + min_next_refresh_ms = remaining; |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + /* Delete stale entries from SB. */ |
| 203 | + HMAP_FOR_EACH_SAFE (synced_entry, hmap_node, &data->synced_entries) { |
| 204 | + if (!synced_entry->stale) { |
| 205 | + continue; |
| 206 | + } |
| 207 | + |
| 208 | + const struct sbrec_mac_binding *sb_mb = |
| 209 | + sbrec_mac_binding_table_get_for_uuid( |
| 210 | + mb_table, &synced_entry->mb_uuid); |
| 211 | + if (sb_mb) { |
| 212 | + sbrec_mac_binding_delete(sb_mb); |
| 213 | + } |
| 214 | + |
| 215 | + evpn_mb_synced_remove(&data->synced_entries, synced_entry); |
| 216 | + } |
| 217 | + |
| 218 | + /* Schedule the waker for the next timestamp refresh. */ |
| 219 | + if (min_next_refresh_ms < INT64_MAX) { |
| 220 | + evpn_mb_sync_waker_schedule(waker, min_next_refresh_ms); |
| 221 | + } |
| 222 | +} |
| 223 | + |
| 224 | +void |
| 225 | +evpn_mac_binding_sync_init(struct ed_type_evpn_mac_binding_sync *data) |
| 226 | +{ |
| 227 | + hmap_init(&data->synced_entries); |
| 228 | + data->sb_changes_pending = false; |
| 229 | +} |
| 230 | + |
| 231 | +void |
| 232 | +evpn_mac_binding_sync_cleanup(struct ed_type_evpn_mac_binding_sync *data) |
| 233 | +{ |
| 234 | + struct evpn_mb_synced_entry *entry; |
| 235 | + HMAP_FOR_EACH_POP (entry, hmap_node, &data->synced_entries) { |
| 236 | + free(entry->logical_port); |
| 237 | + free(entry->ip); |
| 238 | + free(entry); |
| 239 | + } |
| 240 | + hmap_destroy(&data->synced_entries); |
| 241 | +} |
0 commit comments