Replace the single Advertised_Route per VIP (tracked_port = peer LRP,
a chassis-unbound patch port) with one row per (VIP IP, backend LSP).
The backend LSP comes from ip_port_mappings, so it is
chassis-resident wherever the backend is bound.

Multiple LB listeners that share the same VIP IP and the same backend
LSP (e.g. two Services on different ports fronting the same pod) are
deduplicated by the existing SB unique index on (datapath,
logical_port, ip_prefix, tracked_port), so only one Advertised_Route
row is emitted per (VIP IP, backend LSP) pair.

For LBs configured with ip_port_mappings, each backend LSP that
appears in the VIP backend list produces one Advertised_Route row
with tracked_port set to that LSP. Backends without ip_port_mappings
are skipped. When no backend has a mapping, one Advertised_Route
covers the VIP with tracked_port set to the peer LRP (the pre-patch
behavior).

The per-backend split also means
dynamic-routing-redistribute-local-only now correctly restricts
kernel-route installation to chassis that host at least one backend
for the VIP.

Each LB-derived row is tagged with external_ids:source=lb so the
controller can distinguish it from NAT, connected, or static routes
that may share the same ip_prefix.

No SB schema changes are needed.

Also document external_ids:source under Advertised_Route in
ovn-sb.xml, and clarify the "logical-switch-port" value in the
Service_Monitor.type column description.

Signed-off-by: Dmitrii Shcherbakov <[email protected]>
---
 northd/en-advertised-route-sync.c | 154 +++++++++--
 ovn-sb.xml                        |  19 +-
 tests/ovn-northd.at               | 440 +++++++++++++++++++++++++++++-
 3 files changed, 590 insertions(+), 23 deletions(-)

diff --git a/northd/en-advertised-route-sync.c 
b/northd/en-advertised-route-sync.c
index 8e02fdec1..718e49bc9 100644
--- a/northd/en-advertised-route-sync.c
+++ b/northd/en-advertised-route-sync.c
@@ -23,7 +23,11 @@
 #include "en-lr-stateful.h"
 #include "lb.h"
 #include "openvswitch/hmap.h"
+#include "openvswitch/vlog.h"
 #include "ovn-util.h"
+#include "util.h"
+
+VLOG_DEFINE_THIS_MODULE(en_advertised_route_sync);
 
 struct ar_entry {
     struct hmap_node hmap_node;
@@ -106,6 +110,8 @@ ar_entry_find(struct hmap *route_map,
                     tracked_port != route_e->tracked_port->sb) {
                 continue;
             }
+        } else if (route_e->tracked_port) {
+            continue;
         }
 
         return route_e;
@@ -205,6 +211,13 @@ add_redistribute_parsed_route(struct hmap 
*parsed_routes_out,
     /* Parse the prefix (the VIP/FIP). */
     struct in6_addr prefix;
     if (!ip46_parse(ip_address, &prefix)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "Failed to parse IP address '%s' for %s "
+                     "redistribute forwarding route on datapath %s",
+                     ip_address,
+                     source == ROUTE_SOURCE_LB ? "LB" : "NAT",
+                     advertising_od->nbr ? advertising_od->nbr->name
+                                         : "<unknown>");
         return;
     }
     bool is_v6 = !IN6_IS_ADDR_V4MAPPED(&prefix);
@@ -218,6 +231,13 @@ add_redistribute_parsed_route(struct hmap 
*parsed_routes_out,
         nexthop_s = tracked_port->lrp_networks.ipv6_addrs[0].addr_s;
     }
     if (!nexthop_s) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "No %s address on tracked port %s for %s "
+                     "redistribute forwarding route (prefix %s)",
+                     is_v6 ? "IPv6" : "IPv4",
+                     tracked_port->key,
+                     source == ROUTE_SOURCE_LB ? "LB" : "NAT",
+                     ip_address);
         return;
     }
 
@@ -258,7 +278,7 @@ add_redistribute_parsed_route(struct hmap 
*parsed_routes_out,
  * parsed_route on advertising_op->od for each NAT external IP whose
  * nexthop is available from tracked_port (i.e. a peer LRP). This is the
  * connected-neighbour redistribution case where the advertising LR
- * needs to forward to the peer's LR.*/
+ * needs to forward to the peer's LR. */
 static void
 build_nat_route_for_port(const struct ovn_port *advertising_op,
                          const struct lr_nat_record *lr_nat,
@@ -399,6 +419,90 @@ build_nat_connected_routes(
     }
 }
 
+/* For each LB attached to peer_lr_nbr, emit one Advertised_Route per
+ * (VIP IP, backend LSP) pair, plus one forwarding parsed_route per VIP.
+ * Multiple listeners sharing the same VIP IP and backend LSP are
+ * deduplicated by ar_entry_find on the (datapath, logical_port,
+ * ip_prefix, tracked_port) key. When no backend has an ip_port_mappings
+ * entry, one Advertised_Route covers the VIP with fallback_tracked_port
+ * in place of a per-backend LSP. The forwarding route is emitted once
+ * per VIP regardless of backend count: the data-plane forwarding
+ * decision is independent of which backend ends up serving the flow. */
+static void
+build_lb_lr_routes(const struct ovn_port *advertising_op,
+                   const struct ovn_port *fallback_tracked_port,
+                   const struct nbrec_logical_router *peer_lr_nbr,
+                   const struct hmap *lb_datapaths_map,
+                   const struct hmap *ls_ports,
+                   struct hmap *routes,
+                   struct hmap *parsed_routes_out)
+{
+    const struct ovn_datapath *advertising_od = advertising_op->od;
+
+    if (!peer_lr_nbr) {
+        return;
+    }
+
+    for (size_t i = 0; i < peer_lr_nbr->n_load_balancer; i++) {
+        const struct nbrec_load_balancer *nbrec_lb =
+            peer_lr_nbr->load_balancer[i];
+        if (!smap_get_bool(&nbrec_lb->options,
+                           "dynamic-routing-advertise", true)) {
+            continue;
+        }
+        const struct uuid *lb_uuid = &nbrec_lb->header_.uuid;
+        const struct ovn_lb_datapaths *lb_dps =
+            ovn_lb_datapaths_find(lb_datapaths_map, lb_uuid);
+        if (!lb_dps) {
+            continue;
+        }
+        const struct ovn_northd_lb *lb = lb_dps->lb;
+        for (size_t v = 0; v < lb->n_vips; v++) {
+            const struct ovn_lb_vip *vip = &lb->vips[v];
+            const struct ovn_northd_lb_vip *vip_nb = &lb->vips_nb[v];
+
+            if (parsed_routes_out) {
+                add_redistribute_parsed_route(
+                    parsed_routes_out, advertising_od, advertising_op,
+                    fallback_tracked_port, vip->vip_str, ROUTE_SOURCE_LB,
+                    &nbrec_lb->header_);
+            }
+
+            bool emitted_any = false;
+            for (size_t b = 0; b < vip_nb->n_backends; b++) {
+                const char *lsp_name = vip_nb->backends_nb[b].logical_port;
+                if (!lsp_name) {
+                    continue;
+                }
+                const struct ovn_port *backend_op =
+                    ovn_port_find(ls_ports, lsp_name);
+                if (!backend_op) {
+                    continue;
+                }
+                /* The SB unique index on (datapath, logical_port,
+                 * ip_prefix, tracked_port) means only one route per
+                 * (VIP IP, backend LSP) pair can exist. Skip if an
+                 * entry was already added by a previous listener
+                 * sharing the same VIP IP and backend. */
+                if (ar_entry_find(routes, advertising_od->sdp->sb_dp,
+                                  advertising_op->sb, vip->vip_str,
+                                  backend_op->sb)) {
+                    emitted_any = true;
+                    continue;
+                }
+                ar_entry_add(routes, advertising_od, advertising_op,
+                             vip->vip_str, backend_op, ROUTE_SOURCE_LB);
+                emitted_any = true;
+            }
+            if (!emitted_any) {
+                ar_entry_add(routes, advertising_od, advertising_op,
+                             vip->vip_str, fallback_tracked_port,
+                             ROUTE_SOURCE_LB);
+            }
+        }
+    }
+}
+
 /* Own-LR entry point used by the own-LR (gateway-router/DGP) path,
  * which doesn't currently route through a peer LR's LBs. Emits one
  * Advertised_Route per IP in lb_ips with tracked_port as-is.
@@ -445,7 +549,8 @@ build_lb_route_for_port(const struct ovn_port 
*advertising_op,
  * LB VIPs too.*/
 static void
 build_lb_connected_routes(const struct ovn_datapath *od,
-                          const struct lr_stateful_table *lr_stateful_table,
+                          const struct hmap *lb_datapaths_map,
+                          const struct hmap *ls_ports,
                           struct dynamic_routes_data *data)
 {
     const struct ovn_port *op;
@@ -463,13 +568,11 @@ build_lb_connected_routes(const struct ovn_datapath *od,
         /* Track the peer datapath for any changes. */
         dynamic_routes_track_od(data, peer_od);
 
-        const struct lr_stateful_record *lr_stateful_rec;
         /* This is directly connected LR peer. */
         if (peer_od->nbr) {
-            lr_stateful_rec = lr_stateful_table_find_by_uuid(
-                lr_stateful_table, peer_od->key);
-            build_lb_route_for_port(op, op->peer, lr_stateful_rec->lb_ips,
-                                    &data->routes, &data->parsed_routes);
+            build_lb_lr_routes(op, op->peer, peer_od->nbr,
+                               lb_datapaths_map, ls_ports,
+                               &data->routes, &data->parsed_routes);
             continue;
         }
 
@@ -482,11 +585,9 @@ build_lb_connected_routes(const struct ovn_datapath *od,
                  * function.*/
                 continue;
             }
-            lr_stateful_rec = lr_stateful_table_find_by_uuid(
-                lr_stateful_table, rp->peer->od->key);
-
-            build_lb_route_for_port(op, rp->peer, lr_stateful_rec->lb_ips,
-                                    &data->routes, &data->parsed_routes);
+            build_lb_lr_routes(op, rp->peer, rp->peer->od->nbr,
+                               lb_datapaths_map, ls_ports,
+                               &data->routes, &data->parsed_routes);
             /* Track the LR datapath on the other side of LS
              * for any changes. */
             dynamic_routes_track_od(data, rp->peer->od);
@@ -780,7 +881,8 @@ en_dynamic_routes_run(struct engine_node *node, void *data)
 
         build_lb_routes(od, lr_stateful_rec->lb_ips,
                         &dynamic_routes_data->routes);
-        build_lb_connected_routes(od, &lr_stateful_data->table,
+        build_lb_connected_routes(od, &northd_data->lb_datapaths_map,
+                                  &northd_data->ls_ports,
                                   dynamic_routes_data);
     }
 
@@ -989,9 +1091,6 @@ advertised_route_table_sync(
         if (ar_entry_find(&sync_routes, route_e->od->sdp->sb_dp,
                           route_e->op->sb,
                           route_e->ip_prefix, tracked_pb)) {
-            /* We could already have advertised route entry for LRP IP that
-             * corresponds to "snat" when "connected-as-host" is combined
-             * with "nat". Skip it. */
             continue;
         }
         ar_entry_add(&sync_routes, route_e->od, route_e->op,
@@ -1009,10 +1108,19 @@ advertised_route_table_sync(
             sbrec_advertised_route_delete(sb_route);
             continue;
         }
-
-        if (route_e->tracked_port && !sb_route->tracked_port) {
-            sbrec_advertised_route_set_tracked_port(
-                sb_route, route_e->tracked_port->sb);
+        /* Reconcile external_ids:source so the controller can reliably
+         * identify LB-derived rows.  This matters after upgrades (rows
+         * created before this marker existed) and when a non-LB route
+         * reuses a key previously held by an LB-derived row. */
+        const char *cur_source = smap_get(&sb_route->external_ids, "source");
+        if (route_e->source == ROUTE_SOURCE_LB) {
+            if (!cur_source || strcmp(cur_source, "lb")) {
+                sbrec_advertised_route_update_external_ids_setkey(
+                    sb_route, "source", "lb");
+            }
+        } else if (cur_source) {
+            sbrec_advertised_route_update_external_ids_delkey(
+                sb_route, "source");
         }
         hmap_remove(&sync_routes, &route_e->hmap_node);
         ar_entry_free(route_e);
@@ -1028,6 +1136,12 @@ advertised_route_table_sync(
             sbrec_advertised_route_set_tracked_port(sr,
                                                     route_e->tracked_port->sb);
         }
+        if (route_e->source == ROUTE_SOURCE_LB) {
+            struct smap new_ids = SMAP_INITIALIZER(&new_ids);
+            smap_add(&new_ids, "source", "lb");
+            sbrec_advertised_route_set_external_ids(sr, &new_ids);
+            smap_destroy(&new_ids);
+        }
         ar_entry_free(route_e);
     }
 
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 0d3ec6c2c..e70b83a6e 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -5083,8 +5083,8 @@ tcp.flags = RST;
       </p>
 
       <column name="type">
-        The type of the service. Supported values are "load-balancer" and
-        "network-function".
+        The type of the service. Supported values are "load-balancer",
+        "network-function", and "logical-switch-port".
       </column>
 
       <column name="ip">
@@ -5463,6 +5463,21 @@ tcp.flags = RST;
 
     <column name="external_ids">
       See <em>External IDs</em> at the beginning of this document.
+
+      <p>
+        <code>ovn-northd</code> sets the following key on routes
+        derived from Load_Balancer VIPs:
+      </p>
+
+      <dl>
+        <dt><code>source</code></dt>
+        <dd>
+          Set to <code>lb</code> by <code>ovn-northd</code> on routes
+          derived from Load_Balancer VIPs.  Absent on all other routes.
+          <code>ovn-controller</code> uses this key to identify routes
+          eligible for Service_Monitor-based gating.
+        </dd>
+      </dl>
     </column>
   </table>
 
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index da67d54a7..5c921c8d7 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -18330,6 +18330,444 @@ OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute uses backend LSP as tracked_port])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When the LB row's ip_port_mappings populate per-backend LSPs, northd
+# emits one Advertised_Route per (VIP, backend LSP) instead of a single
+# row using the peer LR's gateway LRP. That moves the chassis-locality
+# decision from a chassis-unbound patch port (the peer LRP) to the
+# actual backend port - the controller can then per-chassis install
+# the kernel route via dynamic-routing-redistribute-local-only=true,
+# and gate it on Service_Monitor.status.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+# A backend logical switch hanging off lr1. The two backend LSPs are
+# what we want as tracked_port in Advertised_Route.
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+check ovn-nbctl lsp-add be be-vm2
+check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
+
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
+    -- set Load_Balancer lb0 options:distributed=true \
+        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+        ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1" \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
+
+# Expect two Advertised_Route rows - one per backend LSP.
+check_row_count Advertised_Route 2
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    logical_port=$pb_lr0_up        \
+    tracked_port=$pb_be_vm1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    logical_port=$pb_lr0_up        \
+    tracked_port=$pb_be_vm2
+
+# Forwarding parsed_route is still emitted once - the data-plane
+# decision is independent of backend identity.
+ovn-sbctl lflow-list lr0 > lr0_flows
+AT_CHECK([grep -c 'lr_in_ip_routing.*172.16.1.10/32' lr0_flows], [0], [1
+])
+
+# Without ip_port_mappings, the fallback path emits one row with
+# tracked_port = peer LRP (covered by the existing test). Re-derive
+# that by clearing ip_port_mappings and re-syncing.
+check ovn-nbctl --wait=sb clear Load_Balancer lb0 ip_port_mappings
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    logical_port=$pb_lr0_up        \
+    tracked_port=$pb_lr1_up
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute SCTP per-backend route emission])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# SCTP is outside the Service_Monitor.protocol enum (tcp/udp/icmp), so
+# the per-backend Advertised_Route rows must NOT carry service selector
+# columns.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+check ovn-nbctl lsp-add be be-vm2
+check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
+
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 sctp \
+    -- set Load_Balancer lb0 options:distributed=true \
+        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+        ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1" \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
+
+# Expect two Advertised_Route rows - one per backend LSP.
+check_row_count Advertised_Route 2
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    tracked_port=$pb_be_vm1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    tracked_port=$pb_be_vm2
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute no ip_port_mappings fallback])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When an LB has no ip_port_mappings at all, the fallback path emits
+# one Advertised_Route per VIP with tracked_port set to the peer LRP.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+check ovn-nbctl lsp-add be be-vm2
+check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
+
+# LB with no ip_port_mappings at all.
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
+    -- set Load_Balancer lb0 options:distributed=true \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_lr0_up=$(fetch_column Port_Binding _uuid logical_port=lr0-up)
+pb_lr1_up=$(fetch_column Port_Binding _uuid logical_port=lr1-up)
+
+# One Advertised_Route row with tracked_port = peer LRP (lr1-up),
+# not a per-backend row.
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0          \
+    logical_port=$pb_lr0_up         \
+    tracked_port=$pb_lr1_up
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute partial ip_port_mappings])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# When only some backends have ip_port_mappings entries, only those
+# backends should produce per-backend Advertised_Route rows.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+check ovn-nbctl lsp-add be be-vm2
+check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
+
+# Only map one backend initially.
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
+    -- set Load_Balancer lb0 options:distributed=true \
+        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
+
+# Only one Advertised_Route row - for the mapped backend.
+check_row_count Advertised_Route 1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    tracked_port=$pb_be_vm1
+
+# Add the second mapping and re-sync.
+check ovn-nbctl --wait=sb set Load_Balancer lb0 \
+    ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+    ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1"
+
+# Now expect two rows - both backends mapped.
+check_row_count Advertised_Route 2
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    tracked_port=$pb_be_vm1
+check_row_count Advertised_Route 1 \
+    ip_prefix="172.16.1.10"        \
+    datapath=$datapath_lr0         \
+    tracked_port=$pb_be_vm2
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB redistribute IPv6 per-backend route emission])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# IPv6 variant of the per-backend test: one Advertised_Route row
+# is emitted per backend LSP.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 2001:db8::1/64
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 2001:db8:1::1/64
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 2001:db8:1::10"
+check ovn-nbctl lsp-add be be-vm2
+check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 2001:db8:1::11"
+
+check ovn-nbctl \
+    -- lb-add lb0 [[2001:db8:ffff::10]]:80 
[[2001:db8:1::10]]:80,[[2001:db8:1::11]]:80 \
+    -- set Load_Balancer lb0 options:distributed=true \
+        ip_port_mappings:\"[[2001:db8:1::10]]\"=\"be-vm1:[[2001:db8:1::1]]\" \
+        ip_port_mappings:\"[[2001:db8:1::11]]\"=\"be-vm2:[[2001:db8:1::1]]\" \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+datapath_lr0=$(fetch_column Datapath_Binding _uuid external_ids:name=lr0)
+pb_be_vm1=$(fetch_column Port_Binding _uuid logical_port=be-vm1)
+pb_be_vm2=$(fetch_column Port_Binding _uuid logical_port=be-vm2)
+
+# Expect two Advertised_Route rows - one per backend LSP.
+check_row_count Advertised_Route 2
+check_row_count Advertised_Route 1 \
+    ip_prefix="2001\:db8\:ffff\:\:10" \
+    datapath=$datapath_lr0           \
+    tracked_port=$pb_be_vm1
+check_row_count Advertised_Route 1 \
+    ip_prefix="2001\:db8\:ffff\:\:10" \
+    datapath=$datapath_lr0           \
+    tracked_port=$pb_be_vm2
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - LB route source marker reconciled on existing 
rows])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# Regression test: northd must set external_ids:source=lb on LB-derived
+# Advertised_Route rows.  The marker must persist when the row is reused
+# across recomputes (e.g. when adding a second backend), and northd must
+# restore it on existing rows that lack the marker (upgrade scenario).
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=lb
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+check ovn-nbctl lrp-add lr1 lr1-be 00:00:00:00:00:03 192.168.1.1/24
+check ovn-nbctl ls-add be
+check ovn-nbctl lsp-add-router-port be be-lr1 lr1-be
+check ovn-nbctl lsp-add be be-vm1
+check ovn-nbctl lsp-set-addresses be-vm1 "00:00:00:00:01:01 192.168.1.10"
+check ovn-nbctl lsp-add be be-vm2
+check ovn-nbctl lsp-set-addresses be-vm2 "00:00:00:00:01:02 192.168.1.11"
+
+# Start with one mapped backend.
+check ovn-nbctl \
+    -- lb-add lb0 172.16.1.10:80 192.168.1.10:80,192.168.1.11:80 \
+    -- set Load_Balancer lb0 options:distributed=true \
+        ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+    -- lr-lb-add lr1 lb0
+check ovn-nbctl --wait=sb sync
+
+# One row with source=lb.
+check_row_count Advertised_Route 1
+AT_CHECK([ovn-sbctl --columns=external_ids find Advertised_Route \
+    ip_prefix="172.16.1.10" | grep -q 'source=lb'])
+
+# Add the second backend mapping -- this triggers a northd recompute
+# that reuses the existing row (same tracked_port=be-vm1) and adds
+# a new row for be-vm2.
+check ovn-nbctl --wait=sb set Load_Balancer lb0 \
+    ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+    ip_port_mappings:192.168.1.11="be-vm2:192.168.1.1"
+
+# Both rows must carry source=lb.
+check_row_count Advertised_Route 2
+AT_CHECK([ovn-sbctl --columns=external_ids find Advertised_Route \
+    ip_prefix="172.16.1.10" | grep -c 'source=lb'], [0], [2
+])
+
+# Simulate upgrade from a version that did not set source=lb: strip
+# the marker from existing rows and verify northd restores it via
+# the reconciliation branch on the next recompute.
+AT_CHECK([
+for uuid in $(ovn-sbctl --columns=_uuid --no-headings --bare find \
+    Advertised_Route ip_prefix=172.16.1.10); do
+    (ovn-sbctl remove Advertised_Route "$uuid" external_ids source \
+        2>/dev/null) || true
+done
+], [0], [ignore], [ignore])
+
+# The marker is gone.
+AT_CHECK([! ovn-sbctl --columns=external_ids find Advertised_Route \
+    ip_prefix="172.16.1.10" | grep -q 'source=lb'])
+
+# Trigger a northd recompute by modifying ip_port_mappings (same
+# backends, same VIP IP -- existing rows are reused and the
+# reconciliation branch must restore source=lb).
+check ovn-nbctl --wait=sb set Load_Balancer lb0 \
+    ip_port_mappings:192.168.1.10="be-vm1:192.168.1.1" \
+    ip_port_mappings:192.168.1.11="be-vm2:192.168.1.2"
+
+# The reconciliation branch must have restored source=lb on both rows.
+AT_CHECK([ovn-sbctl --columns=external_ids find Advertised_Route \
+    ip_prefix="172.16.1.10" | grep -c 'source=lb'], [0], [2
+])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([dynamic-routing - non-LB routes do not carry source=lb marker])
+AT_KEYWORDS([dynamic-routing])
+ovn_start
+
+# Regression test: routes from NAT redistribution must not carry
+# external_ids:source=lb, so the controller never applies Service_Monitor
+# gating to non-LB routes.
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl set Logical_Router lr0 \
+    options:dynamic-routing=true       \
+    options:chassis=hv1
+check ovn-nbctl lrp-add lr0 lr0-up 00:00:00:00:00:01
+check ovn-nbctl lrp-set-options lr0-up dynamic-routing-redistribute=nat
+check ovn-nbctl ls-add up
+check ovn-nbctl lsp-add-router-port up up-lr0 lr0-up
+
+check ovn-nbctl lr-add lr1
+check ovn-nbctl lrp-add lr1 lr1-up 00:00:00:00:00:02 10.0.0.1/24
+check ovn-nbctl lrp-set-gateway-chassis lr1-up hv1
+check ovn-nbctl lsp-add-router-port up up-lr1 lr1-up
+check ovn-nbctl --add-route lr-nat-add lr1 dnat_and_snat 172.16.1.10 
192.168.1.10
+check ovn-nbctl --wait=sb sync
+
+# NAT-derived row must not have source=lb.
+check_row_count Advertised_Route 1
+AT_CHECK([! ovn-sbctl --columns=external_ids find Advertised_Route \
+    ip_prefix="172.16.1.10" | grep -q 'source=lb'])
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([dynamic-routing - LB forwarding route updates on nexthop change])
 AT_KEYWORDS([dynamic-routing])
@@ -20022,7 +20460,7 @@ AT_CHECK([grep lr_in_admission lr1_lflows | grep -E 
'priority=5[[05]]' | grep '0
   table=??(lr_in_admission    ), priority=55   , match=(eth.dst == 
02:ac:10:01:00:01 && inport == "lrp1" && (arp)), action=(xreg0[[0..47]] = 
02:ac:10:01:00:01; next;)
 ])
 
-# cr-port options must drop always-redirect=true.
+# cr-port options must clear always-redirect=true.
 AT_CHECK([ovn-sbctl --bare --columns options find Port_Binding 
logical_port=cr-lrp1 | tr ' ' '\n' | sort], [0], [dnl
 distributed-port=lrp1
 ])
-- 
2.53.0


_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to