+
+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