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 <ihrac...@redhat.com>
---
 controller/binding.c | 274 ++++++++++++++++++++++++++++++++++++-------
 controller/lport.c   |  42 ++++---
 northd/northd.c      |  62 ++++++----
 northd/ovn-northd.c  |   4 +-
 ovn-nb.xml           |   4 +-
 ovn-sb.ovsschema     |  19 ++-
 ovn-sb.xml           |  47 +++++++-
 tests/ovn.at         |  89 ++++++++++++++
 8 files changed, 453 insertions(+), 88 deletions(-)

diff --git a/controller/binding.c b/controller/binding.c
index c7a13d5d5..346e4cb76 100644
--- a/controller/binding.c
+++ b/controller/binding.c
@@ -912,6 +912,141 @@ 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) {
+        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 bool
+is_requested_additional_chassis(const struct sbrec_port_binding *pb,
+                                const struct sbrec_chassis *chassis_rec)
+{
+    for (int i = 0; i < pb->n_requested_additional_chassis; i++) {
+        if (pb->requested_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.
  */
@@ -928,37 +1063,56 @@ 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;
-        }
+    if (!pb->requested_chassis || pb->requested_chassis == chassis_rec) {
+        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]);
+            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);
+            }
         }
+    } else if (is_requested_additional_chassis(pb, chassis_rec)) {
+        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 (tracked_datapaths) {
+        update_lport_tracking(pb, tracked_datapaths, 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) {
-        if (sb_readonly) {
-            return false;
-        }
-        sbrec_port_binding_set_encap(pb, encap_rec);
+    if (pb->chassis == chassis_rec) {
+        return update_port_encap_if_needed(
+            pb, chassis_rec, iface_rec, sb_readonly);
+    } else if (is_additional_chassis(pb, chassis_rec)) {
+        return update_port_additional_encap_if_needed(
+            pb, chassis_rec, iface_rec, sb_readonly);
     }
 
     return true;
@@ -972,7 +1126,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) {
@@ -1000,11 +1155,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);
@@ -1023,7 +1209,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,
@@ -1048,7 +1235,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;
@@ -1097,22 +1284,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);
         }
@@ -1234,7 +1420,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);
         }
@@ -1328,7 +1515,7 @@ 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)) {
+        if (!release_lport_main_chassis(pb, !b_ctx_in->ovnsb_idl_txn)) {
             return false;
         }
 
@@ -1363,7 +1550,8 @@ consider_nonvif_lport_(const struct sbrec_port_binding 
*pb,
                            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,
+        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);
     }
diff --git a/controller/lport.c b/controller/lport.c
index 5ad40f6d3..ed63608a2 100644
--- a/controller/lport.c
+++ b/controller/lport.c
@@ -112,25 +112,35 @@ bool
 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 true;
+    }
+
+    for (int i = 0; i < pb->n_requested_additional_chassis; i++) {
+        if (pb->requested_additional_chassis[i] == chassis_rec) {
+            return true;
+        }
+    }
+
     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 true;
+    }
+
+    char *tokstr = xstrdup(requested_chassis_option);
+    char *save_ptr = NULL;
+    char *chassis;
+    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 true;
+        }
     }
-    return !requested_chassis_option || !requested_chassis_option[0]
-           || chassis_rec == pb->requested_chassis;
+    free(tokstr);
+    return false;
 }
 
 const struct sbrec_datapath_binding *
diff --git a/northd/northd.c b/northd/northd.c
index b46862b43..970ff7a15 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -3151,31 +3151,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 95b395931..455148902 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 4d7a23c52..a7d644454 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1017,7 +1017,9 @@
           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.
+          one chassis. If set to a comma separated list, the first entry
+          identifies the main chassis and the latter are any number of
+          additional chassis that are allowed to bind the same port.
         </column>
 
         <column name="options" key="iface-id-ver">
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index 122614dd5..dfdfce71c 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Southbound",
-    "version": "20.21.0",
-    "cksum": "2362446865 26963",
+    "version": "20.22.0",
+    "cksum": "1528689201 27909",
     "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 65bfc9a59..de346d8f9 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>
 
@@ -3243,7 +3280,9 @@ tcp.flags = RST;
         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.
+        one chassis. If set to a comma separated list, the first entry
+        identifies the main chassis and the latter are any number of additional
+        chassis that are allowed to bind the same port.
       </column>
 
       <column name="options" key="iface-id-ver">
diff --git a/tests/ovn.at b/tests/ovn.at
index 0c2fe9f97..0c683fe3b 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -13784,6 +13784,95 @@ 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:requested-chassis for logical port])
 ovn_start
-- 
2.34.1

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to