diff mbox series

[ovs-dev,v8,2/3] Support LSP:options:requested-chassis as a list

Message ID 20220519160834.995994-3-ihrachys@redhat.com
State Superseded, archived
Headers show
Series Support multiple requested-chassis | expand

Checks

Context Check Description
ovsrobot/apply-robot fail apply and check: fail

Commit Message

Ihar Hrachyshka May 19, 2022, 4:08 p.m. UTC
When the option is set to a comma separated list of chassis names, OVN
will attempt to bind the port at any number of other locations in
addition to the main chassis.

This is useful in live migration scenarios where it's important to
prepare the environment for workloads to move to, avoiding costly flow
configuration at the moment of the final port binding location change.

This may also be useful in port mirroring scenarios.

Corresponding database fields (pb->additional_chassis,
pb->requested_additional_chassis, pb->additional_encap) are introduced
as part of the patch. These are similar to fields designed to track the
main chassis (->chassis, ->requested_chassis, ->encap). But because we
support any number of additional chassis, these fields are lists.

Signed-off-by: Ihar Hrachyshka <ihrachys@redhat.com>
---
 NEWS                 |   1 +
 controller/binding.c | 294 ++++++++++++++++++++++++++++++++++++-------
 controller/lport.c   |  46 ++++---
 controller/lport.h   |  11 +-
 northd/northd.c      |  62 ++++++---
 northd/ovn-northd.c  |   4 +-
 ovn-nb.xml           |  20 ++-
 ovn-sb.ovsschema     |  17 ++-
 ovn-sb.xml           |  63 ++++++++--
 tests/ovn.at         | 218 ++++++++++++++++++++++++++++++++
 10 files changed, 632 insertions(+), 104 deletions(-)

Comments

0-day Robot May 19, 2022, 4:23 p.m. UTC | #1
Bleep bloop.  Greetings Ihar Hrachyshka, I am a robot and I have tried out your patch.
Thanks for your contribution.

I encountered some error that I wasn't expecting.  See the details below.


git-am:
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch' to see the failed patch
Patch failed at 0001 Support LSP:options:requested-chassis as a list
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".


Please check this out.  If you feel there has been an error, please email aconole@redhat.com

Thanks,
0-day Robot
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index f18a7d766..856d683f3 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,7 @@  Post v22.03.0
     explicitly request routers to reply to any ARP/ND request for a VIP
     (when set to "all") and only for reachable VIPs (when set to "reachable"
     or by default).
+  - Support list of chassis for Logical_Switch_Port:options:requested-chassis.
 
 OVN v22.03.0 - 11 Mar 2022
 --------------------------
diff --git a/controller/binding.c b/controller/binding.c
index 6bdbe9912..066eec9a5 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -930,6 +930,130 @@  claimed_lport_set_up(const struct sbrec_port_binding *pb,
     }
 }
 
+typedef void (*set_func)(const struct sbrec_port_binding *pb,
+                         const struct sbrec_encap *);
+
+static bool
+update_port_encap_if_needed(const struct sbrec_port_binding *pb,
+                            const struct sbrec_chassis *chassis_rec,
+                            const struct ovsrec_interface *iface_rec,
+                            bool sb_readonly)
+{
+    const struct sbrec_encap *encap_rec =
+        sbrec_get_port_encap(chassis_rec, iface_rec);
+    if ((encap_rec && pb->encap != encap_rec) ||
+        (!encap_rec && pb->encap)) {
+        if (sb_readonly) {
+            return false;
+        }
+        sbrec_port_binding_set_encap(pb, encap_rec);
+    }
+    return true;
+}
+
+static void
+append_additional_encap(const struct sbrec_port_binding *pb,
+                        const struct sbrec_encap *encap)
+{
+    struct sbrec_encap **additional_encap = xmalloc(
+        (pb->n_additional_encap + 1) * (sizeof *additional_encap));
+    memcpy(additional_encap, pb->additional_encap,
+           pb->n_additional_encap * (sizeof *additional_encap));
+    additional_encap[pb->n_additional_encap] = (struct sbrec_encap *) encap;
+    sbrec_port_binding_set_additional_encap(
+        pb, additional_encap, pb->n_additional_encap + 1);
+    free(additional_encap);
+}
+
+static void
+remove_additional_encap_for_chassis(const struct sbrec_port_binding *pb,
+                                    const struct sbrec_chassis *chassis_rec)
+{
+    struct sbrec_encap **additional_encap = xmalloc(
+        pb->n_additional_encap * (sizeof *additional_encap));
+
+    int idx = 0;
+    for (int i = 0; i < pb->n_additional_encap; i++) {
+        if (!strcmp(pb->additional_encap[i]->chassis_name,
+                    chassis_rec->name)) {
+            continue;
+        }
+        additional_encap[idx++] = pb->additional_encap[i];
+    }
+    sbrec_port_binding_set_additional_encap(pb, additional_encap, idx);
+    free(additional_encap);
+}
+
+static bool
+update_port_additional_encap_if_needed(
+    const struct sbrec_port_binding *pb,
+    const struct sbrec_chassis *chassis_rec,
+    const struct ovsrec_interface *iface_rec,
+    bool sb_readonly)
+{
+    const struct sbrec_encap *encap_rec =
+        sbrec_get_port_encap(chassis_rec, iface_rec);
+    if (encap_rec) {
+        for (int i = 0; i < pb->n_additional_encap; i++) {
+            if (pb->additional_encap[i] == encap_rec) {
+                return true;
+            }
+        }
+        if (sb_readonly) {
+            return false;
+        }
+        append_additional_encap(pb, encap_rec);
+    }
+    return true;
+}
+
+static bool
+is_additional_chassis(const struct sbrec_port_binding *pb,
+                      const struct sbrec_chassis *chassis_rec)
+{
+    for (int i = 0; i < pb->n_additional_chassis; i++) {
+        if (pb->additional_chassis[i] == chassis_rec) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static void
+append_additional_chassis(const struct sbrec_port_binding *pb,
+                          const struct sbrec_chassis *chassis_rec)
+{
+    struct sbrec_chassis **additional_chassis = xmalloc(
+        (pb->n_additional_chassis + 1) * (sizeof *additional_chassis));
+    memcpy(additional_chassis, pb->additional_chassis,
+           pb->n_additional_chassis * (sizeof *additional_chassis));
+    additional_chassis[pb->n_additional_chassis] = (
+        (struct sbrec_chassis *) chassis_rec);
+    sbrec_port_binding_set_additional_chassis(
+        pb, additional_chassis, pb->n_additional_chassis + 1);
+    free(additional_chassis);
+}
+
+static void
+remove_additional_chassis(const struct sbrec_port_binding *pb,
+                          const struct sbrec_chassis *chassis_rec)
+{
+    struct sbrec_chassis **additional_chassis = xmalloc(
+        (pb->n_additional_chassis - 1) * (sizeof *additional_chassis));
+    int idx = 0;
+    for (int i = 0; i < pb->n_additional_chassis; i++) {
+        if (pb->additional_chassis[i] == chassis_rec) {
+            continue;
+        }
+        additional_chassis[idx++] = pb->additional_chassis[i];
+    }
+    sbrec_port_binding_set_additional_chassis(
+        pb, additional_chassis, pb->n_additional_chassis - 1);
+    free(additional_chassis);
+
+    remove_additional_encap_for_chassis(pb, chassis_rec);
+}
+
 /* Returns false if lport is not claimed due to 'sb_readonly'.
  * Returns true otherwise.
  */
@@ -946,40 +1070,65 @@  claim_lport(const struct sbrec_port_binding *pb,
         claimed_lport_set_up(pb, parent_pb, chassis_rec, notify_up, if_mgr);
     }
 
-    if (pb->chassis != chassis_rec) {
-        if (sb_readonly) {
-            return false;
-        }
+    enum can_bind can_bind = lport_can_bind_on_this_chassis(chassis_rec, pb);
+    bool update_tracked = false;
 
-        if (pb->chassis) {
-            VLOG_INFO("Changing chassis for lport %s from %s to %s.",
-                    pb->logical_port, pb->chassis->name,
-                    chassis_rec->name);
-        } else {
-            VLOG_INFO("Claiming lport %s for this chassis.", pb->logical_port);
-        }
-        for (int i = 0; i < pb->n_mac; i++) {
-            VLOG_INFO("%s: Claiming %s", pb->logical_port, pb->mac[i]);
+    if (can_bind == CAN_BIND_AS_MAIN) {
+        if (pb->chassis != chassis_rec) {
+            if (sb_readonly) {
+                return false;
+            }
+
+            if (pb->chassis) {
+                VLOG_INFO("Changing chassis for lport %s from %s to %s.",
+                        pb->logical_port, pb->chassis->name,
+                        chassis_rec->name);
+            } else {
+                VLOG_INFO("Claiming lport %s for this chassis.",
+                          pb->logical_port);
+            }
+            for (int i = 0; i < pb->n_mac; i++) {
+                VLOG_INFO("%s: Claiming %s", pb->logical_port, pb->mac[i]);
+            }
+
+            sbrec_port_binding_set_chassis(pb, chassis_rec);
+            if (is_additional_chassis(pb, chassis_rec)) {
+                remove_additional_chassis(pb, chassis_rec);
+            }
+            update_tracked = true;
         }
+    } else if (can_bind == CAN_BIND_AS_ADDITIONAL) {
+        if (!is_additional_chassis(pb, chassis_rec)) {
+            if (sb_readonly) {
+                return false;
+            }
 
-        sbrec_port_binding_set_chassis(pb, chassis_rec);
+            VLOG_INFO("Claiming lport %s for this additional chassis.",
+                      pb->logical_port);
+            for (int i = 0; i < pb->n_mac; i++) {
+                VLOG_INFO("%s: Claiming %s", pb->logical_port, pb->mac[i]);
+            }
 
-        if (tracked_datapaths) {
-            update_lport_tracking(pb, tracked_datapaths, true);
+            append_additional_chassis(pb, chassis_rec);
+            if (pb->chassis == chassis_rec) {
+                sbrec_port_binding_set_chassis(pb, NULL);
+            }
+            update_tracked = true;
         }
     }
 
-    /* Check if the port encap binding, if any, has changed */
-    struct sbrec_encap *encap_rec =
-        sbrec_get_port_encap(chassis_rec, iface_rec);
-    if ((encap_rec && pb->encap != encap_rec) ||
-        (!encap_rec && pb->encap)) {
-        if (sb_readonly) {
-            return false;
-        }
-        sbrec_port_binding_set_encap(pb, encap_rec);
+    if (update_tracked && tracked_datapaths) {
+        update_lport_tracking(pb, tracked_datapaths, true);
     }
 
+    /* Check if the port encap binding, if any, has changed */
+    if (can_bind == CAN_BIND_AS_MAIN) {
+        return update_port_encap_if_needed(
+            pb, chassis_rec, iface_rec, sb_readonly);
+    } else if (can_bind == CAN_BIND_AS_ADDITIONAL) {
+        return update_port_additional_encap_if_needed(
+            pb, chassis_rec, iface_rec, sb_readonly);
+    }
     return true;
 }
 
@@ -991,7 +1140,8 @@  claim_lport(const struct sbrec_port_binding *pb,
  * Caller should make sure that this is the case.
  */
 static bool
-release_lport_(const struct sbrec_port_binding *pb, bool sb_readonly)
+release_lport_main_chassis(const struct sbrec_port_binding *pb,
+                           bool sb_readonly)
 {
     if (pb->encap) {
         if (sb_readonly) {
@@ -1019,11 +1169,42 @@  release_lport_(const struct sbrec_port_binding *pb, bool sb_readonly)
 }
 
 static bool
-release_lport(const struct sbrec_port_binding *pb, bool sb_readonly,
+release_lport_additional_chassis(const struct sbrec_port_binding *pb,
+                                 const struct sbrec_chassis *chassis_rec,
+                                 bool sb_readonly)
+{
+    if (pb->additional_encap) {
+        if (sb_readonly) {
+            return false;
+        }
+        remove_additional_encap_for_chassis(pb, chassis_rec);
+    }
+
+    if (is_additional_chassis(pb, chassis_rec)) {
+        if (sb_readonly) {
+            return false;
+        }
+        remove_additional_chassis(pb, chassis_rec);
+    }
+
+    VLOG_INFO("Releasing lport %s from this additional chassis.",
+              pb->logical_port);
+    return true;
+}
+
+static bool
+release_lport(const struct sbrec_port_binding *pb,
+              const struct sbrec_chassis *chassis_rec, bool sb_readonly,
               struct hmap *tracked_datapaths, struct if_status_mgr *if_mgr)
 {
-    if (!release_lport_(pb, sb_readonly)) {
-        return false;
+    if (pb->chassis == chassis_rec) {
+        if (!release_lport_main_chassis(pb, sb_readonly)) {
+            return false;
+        }
+    } else if (is_additional_chassis(pb, chassis_rec)) {
+        if (!release_lport_additional_chassis(pb, chassis_rec, sb_readonly)) {
+            return false;
+        }
     }
 
     update_lport_tracking(pb, tracked_datapaths, false);
@@ -1042,7 +1223,8 @@  is_binding_lport_this_chassis(struct binding_lport *b_lport,
                               const struct sbrec_chassis *chassis)
 {
     return (b_lport && b_lport->pb && chassis &&
-            b_lport->pb->chassis == chassis);
+            (b_lport->pb->chassis == chassis
+             || is_additional_chassis(b_lport->pb, chassis)));
 }
 
 /* Returns 'true' if the 'lbinding' has binding lports of type LP_CONTAINER,
@@ -1067,7 +1249,7 @@  release_binding_lport(const struct sbrec_chassis *chassis_rec,
 {
     if (is_binding_lport_this_chassis(b_lport, chassis_rec)) {
         remove_related_lport(b_lport->pb, b_ctx_out);
-        if (!release_lport(b_lport->pb, sb_readonly,
+        if (!release_lport(b_lport->pb, chassis_rec, sb_readonly,
                            b_ctx_out->tracked_dp_bindings,
                            b_ctx_out->if_mgr)) {
             return false;
@@ -1116,22 +1298,21 @@  consider_vif_lport_(const struct sbrec_port_binding *pb,
         } else {
             /* We could, but can't claim the lport. */
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_INFO_RL(&rl,
-                             "Not claiming lport %s, chassis %s "
-                             "requested-chassis %s",
-                             pb->logical_port,
-                             b_ctx_in->chassis_rec->name,
-                             pb->requested_chassis ?
-                             pb->requested_chassis->name : "(option points at "
-                                                           "non-existent "
-                                                           "chassis)");
+            const char *requested_chassis_option = smap_get(
+                &pb->options, "requested-chassis");
+            VLOG_INFO_RL(&rl,
+                "Not claiming lport %s, chassis %s requested-chassis %s",
+                pb->logical_port, b_ctx_in->chassis_rec->name,
+                requested_chassis_option ? requested_chassis_option : "[]");
         }
     }
 
-    if (pb->chassis == b_ctx_in->chassis_rec) {
+    if (pb->chassis == b_ctx_in->chassis_rec
+            || is_additional_chassis(pb, b_ctx_in->chassis_rec)) {
         /* Release the lport if there is no lbinding. */
         if (!lbinding_set || !can_bind) {
-            return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
+            return release_lport(pb, b_ctx_in->chassis_rec,
+                                 !b_ctx_in->ovnsb_idl_txn,
                                  b_ctx_out->tracked_dp_bindings,
                                  b_ctx_out->if_mgr);
         }
@@ -1253,7 +1434,8 @@  consider_container_lport(const struct sbrec_port_binding *pb,
          * if it was bound earlier. */
         if (is_binding_lport_this_chassis(container_b_lport,
                                           b_ctx_in->chassis_rec)) {
-            return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
+            return release_lport(pb, b_ctx_in->chassis_rec,
+                                 !b_ctx_in->ovnsb_idl_txn,
                                  b_ctx_out->tracked_dp_bindings,
                                  b_ctx_out->if_mgr);
         }
@@ -1346,11 +1528,20 @@  consider_localport(const struct sbrec_port_binding *pb,
 
     /* If the port binding is claimed, then release it as localport is claimed
      * by any ovn-controller. */
-    if (pb->chassis == b_ctx_in->chassis_rec) {
-        if (!release_lport_(pb, !b_ctx_in->ovnsb_idl_txn)) {
+    enum can_bind can_bind = lport_can_bind_on_this_chassis(
+        b_ctx_in->chassis_rec, pb);
+    if (can_bind == CAN_BIND_AS_MAIN) {
+        if (!release_lport_main_chassis(pb, !b_ctx_in->ovnsb_idl_txn)) {
+            return false;
+        }
+    } else if (can_bind == CAN_BIND_AS_ADDITIONAL) {
+        if (!release_lport_additional_chassis(pb, b_ctx_in->chassis_rec,
+                                              !b_ctx_in->ovnsb_idl_txn)) {
             return false;
         }
+    }
 
+    if (can_bind) {
         remove_related_lport(pb, b_ctx_out);
     }
 
@@ -1381,8 +1572,12 @@  consider_nonvif_lport_(const struct sbrec_port_binding *pb,
                            !b_ctx_in->ovnsb_idl_txn, false,
                            b_ctx_out->tracked_dp_bindings,
                            b_ctx_out->if_mgr);
-    } else if (pb->chassis == b_ctx_in->chassis_rec) {
-        return release_lport(pb, !b_ctx_in->ovnsb_idl_txn,
+    }
+
+    if (pb->chassis == b_ctx_in->chassis_rec ||
+            is_additional_chassis(pb, b_ctx_in->chassis_rec)) {
+        return release_lport(pb, b_ctx_in->chassis_rec,
+                             !b_ctx_in->ovnsb_idl_txn,
                              b_ctx_out->tracked_dp_bindings,
                              b_ctx_out->if_mgr);
     }
@@ -1709,6 +1904,11 @@  binding_cleanup(struct ovsdb_idl_txn *ovnsb_idl_txn,
             sbrec_port_binding_set_chassis(binding_rec, NULL);
             any_changes = true;
         }
+        if (is_additional_chassis(binding_rec, chassis_rec)) {
+            remove_additional_encap_for_chassis(binding_rec, chassis_rec);
+            remove_additional_chassis(binding_rec, chassis_rec);
+            any_changes = true;
+        }
     }
 
     if (any_changes) {
diff --git a/controller/lport.c b/controller/lport.c
index 5ad40f6d3..d88215715 100644
--- a/controller/lport.c
+++ b/controller/lport.c
@@ -108,29 +108,41 @@  lport_get_l3gw_peer(const struct sbrec_port_binding *pb,
     return get_peer_lport(pb, sbrec_port_binding_by_name);
 }
 
-bool
+enum can_bind
 lport_can_bind_on_this_chassis(const struct sbrec_chassis *chassis_rec,
                                const struct sbrec_port_binding *pb)
 {
-    /* We need to check for presence of the requested-chassis option in
-     * addittion to checking the pb->requested_chassis column because this
-     * column will be set to NULL whenever the option points to a non-existent
-     * chassis.  As the controller routinely clears its own chassis record this
-     * might occur more often than one might think. */
+    if (pb->requested_chassis == chassis_rec) {
+        return CAN_BIND_AS_MAIN;
+    }
+
+    for (int i = 0; i < pb->n_requested_additional_chassis; i++) {
+        if (pb->requested_additional_chassis[i] == chassis_rec) {
+            return CAN_BIND_AS_ADDITIONAL;
+        }
+    }
+
     const char *requested_chassis_option = smap_get(&pb->options,
                                                     "requested-chassis");
-    if (requested_chassis_option && requested_chassis_option[0]
-        && !pb->requested_chassis) {
-        /* The requested-chassis option is set, but the requested_chassis
-         * column is not filled.  This means that the chassis the option
-         * points to is currently not running, or is in the process of starting
-         * up.  In this case we must fall back to comparing the strings to
-         * avoid release/claim thrashing. */
-        return !strcmp(requested_chassis_option, chassis_rec->name)
-               || !strcmp(requested_chassis_option, chassis_rec->hostname);
+    if (!requested_chassis_option || !strcmp("", requested_chassis_option)) {
+        return CAN_BIND_AS_MAIN;
+    }
+
+    char *tokstr = xstrdup(requested_chassis_option);
+    char *save_ptr = NULL;
+    char *chassis;
+    enum can_bind can_bind = CAN_BIND_AS_MAIN;
+    for (chassis = strtok_r(tokstr, ",", &save_ptr); chassis != NULL;
+         chassis = strtok_r(NULL, ",", &save_ptr)) {
+        if (!strcmp(chassis, chassis_rec->name)
+                || !strcmp(chassis, chassis_rec->hostname)) {
+            free(tokstr);
+            return can_bind;
+        }
+        can_bind = CAN_BIND_AS_ADDITIONAL;
     }
-    return !requested_chassis_option || !requested_chassis_option[0]
-           || chassis_rec == pb->requested_chassis;
+    free(tokstr);
+    return CANNOT_BIND;
 }
 
 const struct sbrec_datapath_binding *
diff --git a/controller/lport.h b/controller/lport.h
index 4716c58f9..115881655 100644
--- a/controller/lport.h
+++ b/controller/lport.h
@@ -43,8 +43,15 @@  const struct sbrec_port_binding *lport_lookup_by_key(
     struct ovsdb_idl_index *sbrec_port_binding_by_key,
     uint64_t dp_key, uint64_t port_key);
 
-bool lport_can_bind_on_this_chassis(const struct sbrec_chassis *chassis_rec,
-                                   const struct sbrec_port_binding *pb);
+enum can_bind {
+    CANNOT_BIND = 0,
+    CAN_BIND_AS_MAIN,
+    CAN_BIND_AS_ADDITIONAL,
+};
+
+enum can_bind
+lport_can_bind_on_this_chassis(const struct sbrec_chassis *chassis_rec,
+                               const struct sbrec_port_binding *pb);
 
 const struct sbrec_datapath_binding *datapath_lookup_by_key(
     struct ovsdb_idl_index *sbrec_datapath_binding_by_key, uint64_t dp_key);
diff --git a/northd/northd.c b/northd/northd.c
index 6138d42ad..3e4c90957 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -3244,31 +3244,53 @@  ovn_port_update_sbrec_chassis(
         const struct ovn_port *op)
 {
     const char *requested_chassis; /* May be NULL. */
-    bool reset_requested_chassis = false;
+
+    int n_requested_chassis = 0;
+    struct sbrec_chassis **requested_chassis_sb = xcalloc(
+        n_requested_chassis, sizeof *requested_chassis_sb);
+
     requested_chassis = smap_get(&op->nbsp->options,
                                  "requested-chassis");
     if (requested_chassis) {
-        const struct sbrec_chassis *chassis = chassis_lookup(
-            sbrec_chassis_by_name, sbrec_chassis_by_hostname,
-            requested_chassis);
-        if (chassis) {
-            sbrec_port_binding_set_requested_chassis(op->sb, chassis);
-        } else {
-            reset_requested_chassis = true;
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(
-                1, 1);
-            VLOG_WARN_RL(
-                &rl,
-                "Unknown chassis '%s' set as "
-                "options:requested-chassis on LSP '%s'.",
-                requested_chassis, op->nbsp->name);
-        }
-    } else if (op->sb->requested_chassis) {
-        reset_requested_chassis = true;
-    }
-    if (reset_requested_chassis) {
+        char *tokstr = xstrdup(requested_chassis);
+        char *save_ptr = NULL;
+        char *chassis;
+        for (chassis = strtok_r(tokstr, ",", &save_ptr); chassis != NULL;
+             chassis = strtok_r(NULL, ",", &save_ptr)) {
+            const struct sbrec_chassis *chassis_sb = chassis_lookup(
+                sbrec_chassis_by_name, sbrec_chassis_by_hostname, chassis);
+            if (chassis_sb) {
+                requested_chassis_sb = xrealloc(
+                    requested_chassis_sb,
+                    ++n_requested_chassis * (sizeof *requested_chassis_sb));
+                requested_chassis_sb[n_requested_chassis - 1] = (
+                    (struct sbrec_chassis *) chassis_sb);
+            } else {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(
+                    1, 1);
+                VLOG_WARN_RL(
+                    &rl,
+                    "Unknown chassis '%s' set in "
+                    "options:requested-chassis on LSP '%s'.",
+                    chassis, op->nbsp->name);
+            }
+        }
+        free(tokstr);
+    }
+
+    if (n_requested_chassis > 0) {
+        sbrec_port_binding_set_requested_chassis(op->sb,
+                                                 *requested_chassis_sb);
+    } else {
         sbrec_port_binding_set_requested_chassis(op->sb, NULL);
     }
+    if (n_requested_chassis > 1) {
+        sbrec_port_binding_set_requested_additional_chassis(
+            op->sb, &requested_chassis_sb[1], n_requested_chassis - 1);
+    } else {
+        sbrec_port_binding_set_requested_additional_chassis(op->sb, NULL, 0);
+    }
+    free(requested_chassis_sb);
 }
 
 static void
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 0a0f85010..1cfc2024d 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -100,7 +100,9 @@  static const char *rbac_fdb_update[] =
 static const char *rbac_port_binding_auth[] =
     {""};
 static const char *rbac_port_binding_update[] =
-    {"chassis", "encap", "up", "virtual_parent"};
+    {"chassis", "additional_chassis",
+     "encap", "additional_encap",
+     "up", "virtual_parent"};
 
 static const char *rbac_mac_binding_auth[] =
     {""};
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 225e0cf9c..5e521df60 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1020,12 +1020,20 @@ 
         </p>
 
         <column name="options" key="requested-chassis">
-          If set, identifies a specific chassis (by name or hostname) that
-          is allowed to bind this port. Using this option will prevent
-          thrashing between two chassis trying to bind the same port during
-          a live migration. It can also prevent similar thrashing due to a
-          mis-configuration, if a port is accidentally created on more than
-          one chassis.
+          <p>
+            If set, identifies a specific chassis (by name or hostname) that
+            is allowed to bind this port. Using this option will prevent
+            thrashing between two chassis trying to bind the same port during
+            a live migration. It can also prevent similar thrashing due to a
+            mis-configuration, if a port is accidentally created on more than
+            one chassis.
+          </p>
+
+          <p>
+            If set to a comma separated list, the first entry identifies the
+            main chassis and the rest are one or more additional chassis that
+            are allowed to bind the same port.
+          </p>
         </column>
 
         <column name="options" key="iface-id-ver">
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index 66664c840..fcd74b918 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Southbound",
     "version": "20.22.0",
-    "cksum": "1686121686 27471",
+    "cksum": "2805878321 28417",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -218,10 +218,18 @@ 
                                              "refTable": "Chassis",
                                              "refType": "weak"},
                                      "min": 0, "max": 1}},
+                "additional_chassis": {"type": {"key": {"type": "uuid",
+                                                        "refTable": "Chassis",
+                                                        "refType": "weak"},
+                                               "min": 0, "max": "unlimited"}},
                 "encap": {"type": {"key": {"type": "uuid",
                                             "refTable": "Encap",
                                              "refType": "weak"},
                                     "min": 0, "max": 1}},
+                "additional_encap": {"type": {"key": {"type": "uuid",
+                                                      "refTable": "Encap",
+                                                      "refType": "weak"},
+                                    "min": 0, "max": "unlimited"}},
                 "mac": {"type": {"key": "string",
                                  "min": 0,
                                  "max": "unlimited"}},
@@ -236,7 +244,12 @@ 
                 "requested_chassis": {"type": {"key": {"type": "uuid",
                                                        "refTable": "Chassis",
                                                        "refType": "weak"},
-                                               "min": 0, "max": 1}}},
+                                               "min": 0, "max": 1}},
+                "requested_additional_chassis": {
+                                      "type": {"key": {"type": "uuid",
+                                                       "refTable": "Chassis",
+                                                       "refType": "weak"},
+                                               "min": 0, "max": "unlimited"}}},
             "indexes": [["datapath", "tunnel_key"], ["logical_port"]],
             "isRoot": true},
         "MAC_Binding": {
diff --git a/ovn-sb.xml b/ovn-sb.xml
index b0b11b8ee..0393d1220 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -2867,9 +2867,17 @@  tcp.flags = RST;
       </column>
 
       <column name="encap">
-        Points to supported encapsulation configurations to transmit
-        logical dataplane packets to this chassis.  Each entry is a <ref
-        table="Encap"/> record that describes the configuration.
+        Points to preferred encapsulation configuration to transmit
+        logical dataplane packets to this chassis. The entry is reference to
+        a <ref table="Encap"/> record.
+      </column>
+
+      <column name="additional_encap">
+        Points to preferred encapsulation configuration to transmit
+        logical dataplane packets to this additional chassis. The entry is
+        reference to a <ref table="Encap"/> record.
+
+        See also <ref column="additional_chassis"/>.
       </column>
 
       <column name="chassis">
@@ -2921,6 +2929,13 @@  tcp.flags = RST;
 
       </column>
 
+      <column name="additional_chassis">
+        The meaning of this column is the same as for the
+        <ref column="chassis"/>. The column is used to track an additional
+        physical location of the logical port. Used with regular (empty
+        <ref column="type"/>) port bindings.
+      </column>
+
       <column name="gateway_chassis">
         <p>
           A list of <ref table="Gateway_Chassis"/>.
@@ -3079,6 +3094,28 @@  tcp.flags = RST;
         db="OVN_Northbound"/>
         is defined and contains a string matching the name or hostname of an
         existing chassis.
+
+        See also
+        <ref table="Port_Binding" column="requested_additional_chassis"/>.
+      </column>
+      <column name="requested_additional_chassis">
+        This column exists so that the ovn-controller can effectively monitor
+        all <ref table="Port_Binding"/> records destined for it, and is a
+        supplement to the <ref
+        table="Port_Binding"
+        column="options"
+        key="requested-chassis"/> option when multiple chassis are listed.
+
+        This column must be a list of
+        <ref table="Chassis"/> records.  This is populated by
+        <code>ovn-northd</code> when the <ref
+        table="Logical_Switch_Port"
+        column="options"
+        key="requested-chassis"
+        db="OVN_Northbound"/>
+        is defined as a list of chassis names or hostnames.
+
+        See also <ref table="Port_Binding" column="requested_chassis"/>.
       </column>
     </group>
 
@@ -3238,12 +3275,20 @@  tcp.flags = RST;
       </p>
 
       <column name="options" key="requested-chassis">
-        If set, identifies a specific chassis (by name or hostname) that
-        is allowed to bind this port. Using this option will prevent
-        thrashing between two chassis trying to bind the same port during
-        a live migration. It can also prevent similar thrashing due to a
-        mis-configuration, if a port is accidentally created on more than
-        one chassis.
+        <p>
+          If set, identifies a specific chassis (by name or hostname) that
+          is allowed to bind this port. Using this option will prevent
+          thrashing between two chassis trying to bind the same port during
+          a live migration. It can also prevent similar thrashing due to a
+          mis-configuration, if a port is accidentally created on more than
+          one chassis.
+        </p>
+
+        <p>
+          If set to a comma separated list, the first entry identifies the main
+          chassis and the rest are one or more additional chassis that are
+          allowed to bind the same port.
+        </p>
       </column>
 
       <column name="options" key="iface-id-ver">
diff --git a/tests/ovn.at b/tests/ovn.at
index bb00d8e1a..a703effff 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -13968,6 +13968,224 @@  OVN_CLEANUP([hv1])
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([options:multiple requested-chassis for logical port])
+ovn_start
+
+net_add n1
+
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.11
+
+sim_add hv2
+as hv2
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.12
+
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add ls0 lsp0
+
+# Allow only chassis hv1 to bind logical port lsp0.
+check ovn-nbctl lsp-set-options lsp0 requested-chassis=hv1
+
+as hv1 check ovs-vsctl -- add-port br-int lsp0 -- \
+    set Interface lsp0 external-ids:iface-id=lsp0
+as hv2 check ovs-vsctl -- add-port br-int lsp0 -- \
+    set Interface lsp0 external-ids:iface-id=lsp0
+
+wait_row_count Chassis 1 name=hv1
+wait_row_count Chassis 1 name=hv2
+hv1_uuid=$(fetch_column Chassis _uuid name=hv1)
+hv2_uuid=$(fetch_column Chassis _uuid name=hv2)
+
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "" Port_Binding additional_chassis logical_port=lsp0
+wait_column "" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Request port binding at an additional chassis
+check ovn-nbctl lsp-set-options lsp0 \
+    requested-chassis=hv1,hv2
+
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv2_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Check that setting iface:encap-ip populates Port_Binding:additional_encap
+wait_row_count Encap 2 chassis_name=hv1
+wait_row_count Encap 2 chassis_name=hv2
+encap_hv1_uuid=$(fetch_column Encap _uuid chassis_name=hv1 type=geneve)
+encap_hv2_uuid=$(fetch_column Encap _uuid chassis_name=hv2 type=geneve)
+
+wait_column "" Port_Binding encap logical_port=lsp0
+wait_column "" Port_Binding additional_encap logical_port=lsp0
+
+as hv1 check ovs-vsctl -- \
+    set Interface lsp0 external-ids:encap-ip=192.168.0.11
+as hv2 check ovs-vsctl -- \
+    set Interface lsp0 external-ids:encap-ip=192.168.0.12
+
+wait_column "$encap_hv1_uuid" Port_Binding encap logical_port=lsp0
+wait_column "$encap_hv2_uuid" Port_Binding additional_encap logical_port=lsp0
+
+# Complete moving the binding to the new location
+check ovn-nbctl lsp-set-options lsp0 requested-chassis=hv2
+
+wait_column "$hv2_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv2_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "" Port_Binding additional_chassis logical_port=lsp0
+wait_column "" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Check that additional_encap is cleared
+wait_column "" Port_Binding additional_encap logical_port=lsp0
+
+# Check that abrupted port migration clears additional_encap
+check ovn-nbctl lsp-set-options lsp0 \
+    requested-chassis=hv2,hv1
+wait_column "$hv2_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv2_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+check ovn-nbctl lsp-set-options lsp0 requested-chassis=hv2
+wait_column "" Port_Binding additional_encap logical_port=lsp0
+
+OVN_CLEANUP([hv1],[hv2])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([options:multiple requested-chassis for logical port: change chassis role])
+ovn_start
+
+net_add n1
+
+for i in 1 2 3; do
+    sim_add hv$i
+    as hv$i
+    check ovs-vsctl add-br br-phys
+    ovn_attach n1 br-phys 192.168.0.1$i
+done
+
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add ls0 lsp0
+
+# Bind the port to all of them
+check ovn-nbctl lsp-set-options lsp0 requested-chassis=hv1,hv2,hv3
+
+for i in 1 2 3; do
+    as hv$i check ovs-vsctl -- add-port br-int lsp0 -- \
+        set Interface lsp0 external-ids:iface-id=lsp0
+done
+
+for i in 1 2 3; do
+    wait_row_count Chassis 1 name=hv$i
+done
+hv1_uuid=$(fetch_column Chassis _uuid name=hv1)
+hv2_uuid=$(fetch_column Chassis _uuid name=hv2)
+hv3_uuid=$(fetch_column Chassis _uuid name=hv3)
+
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Now swap hv1 and hv2 chassis roles: main <-> additional
+check ovn-nbctl lsp-set-options lsp0 requested-chassis=hv3,hv2,hv1
+
+wait_column "$hv3_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv3_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv1_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv1_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+OVN_CLEANUP([hv1],[hv2],[hv3])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([options:multiple requested-chassis for logical port: unclaimed behavior])
+ovn_start
+
+net_add n1
+
+for i in 1 2 3; do
+    sim_add hv$i
+    as hv$i
+    check ovs-vsctl add-br br-phys
+    ovn_attach n1 br-phys 192.168.0.1$i
+done
+
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add ls0 lsp0
+
+# Bind the port to all of them
+check ovn-nbctl lsp-set-options lsp0 requested-chassis=hv1,hv2,hv3
+
+for i in 1 2 3; do
+    wait_row_count Chassis 1 name=hv$i
+done
+hv1_uuid=$(fetch_column Chassis _uuid name=hv1)
+hv2_uuid=$(fetch_column Chassis _uuid name=hv2)
+hv3_uuid=$(fetch_column Chassis _uuid name=hv3)
+
+# Port not claimed by any of chassis
+wait_column "" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Now claim on main chassis
+as hv1 check ovs-vsctl -- add-port br-int lsp0 -- \
+    set Interface lsp0 external-ids:iface-id=lsp0
+
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Now claim on the second additional chassis
+as hv3 check ovs-vsctl -- add-port br-int lsp0 -- \
+    set Interface lsp0 external-ids:iface-id=lsp0
+
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv3_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Now claim the other additional chassis
+as hv2 check ovs-vsctl -- add-port br-int lsp0 -- \
+    set Interface lsp0 external-ids:iface-id=lsp0
+
+wait_column "$hv1_uuid" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Now unclaim from the main chassis
+as hv1 check ovs-vsctl -- del-port br-int lsp0
+
+wait_column "" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+# Now unclaim from an additional chassis
+as hv3 check ovs-vsctl -- del-port br-int lsp0
+
+wait_column "" Port_Binding chassis logical_port=lsp0
+wait_column "$hv1_uuid" Port_Binding requested_chassis logical_port=lsp0
+wait_column "$hv2_uuid" Port_Binding additional_chassis logical_port=lsp0
+wait_column "$hv2_uuid $hv3_uuid" Port_Binding requested_additional_chassis logical_port=lsp0
+
+OVN_CLEANUP([hv1],[hv2],[hv3])
+
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD([
 AT_SETUP([options:requested-chassis for logical port])
 ovn_start