Introduce the capability to actively refresh mac_binding entries available
in OFTABLE_MAC_BINDING table when they are inactive for more than the
configured threshold. Add the OFTABLE_MAC_BINDING table stats
monitoring. This feature avoids possible unnecessary mac_entry removals
if the destination is still alive but inactive for more than the configured
value.
Reported-at: https://issues.redhat.com/browse/FDP-1135
Signed-off-by: Lorenzo Bianconi <[email protected]>
---
controller/mac-cache.c | 112 ++++++++++++++++++++++++++++++++++--
controller/mac-cache.h | 26 +++++++--
controller/ovn-controller.c | 40 ++++++++++---
controller/pinctrl.c | 8 +--
controller/pinctrl.h | 7 +++
controller/statctrl.c | 25 +++++++-
tests/ovn.at | 104 +++++++++++++++++++++++++++++++--
7 files changed, 293 insertions(+), 29 deletions(-)
diff --git a/controller/mac-cache.c b/controller/mac-cache.c
index 9d05c1950..087767304 100644
--- a/controller/mac-cache.c
+++ b/controller/mac-cache.c
@@ -16,6 +16,7 @@
#include <config.h>
#include <stdbool.h>
+#include "lflow.h"
#include "lib/mac-binding-index.h"
#include "local_data.h"
#include "lport.h"
@@ -24,6 +25,7 @@
#include "openvswitch/vlog.h"
#include "ovn/logical-fields.h"
#include "ovn-sb-idl.h"
+#include "pinctrl.h"
VLOG_DEFINE_THIS_MODULE(mac_cache);
@@ -160,17 +162,25 @@ mac_cache_thresholds_clear(struct mac_cache_data *data)
/* MAC binding. */
void
mac_binding_add(struct hmap *map, struct mac_binding_data mb_data,
- const struct sbrec_mac_binding *smb, long long timestamp)
+ const struct sbrec_mac_binding *smb,
+ const struct sbrec_port_binding *pb,
+ long long timestamp)
{
struct mac_binding *mb = mac_binding_find(map, &mb_data);
if (!mb) {
- mb = xmalloc(sizeof *mb);
+ mb = xzalloc(sizeof *mb);
hmap_insert(map, &mb->hmap_node, mac_binding_data_hash(&mb_data));
}
mb->data = mb_data;
mb->sbrec = smb;
mb->timestamp = timestamp;
+ if (pb) {
+ destroy_lport_addresses(&mb->laddr);
+ if (extract_lsp_addresses(pb->mac[0], &mb->laddr)) {
+ mb->tunnel_key = pb->tunnel_key;
+ }
+ }
mac_binding_update_log("Added", &mb_data, false, NULL, 0, 0);
}
@@ -179,6 +189,7 @@ mac_binding_remove(struct hmap *map, struct mac_binding *mb)
{
mac_binding_update_log("Removed", &mb->data, false, NULL, 0, 0);
hmap_remove(map, &mb->hmap_node);
+ destroy_lport_addresses(&mb->laddr);
free(mb);
}
@@ -237,6 +248,7 @@ mac_bindings_clear(struct hmap *map)
{
struct mac_binding *mb;
HMAP_FOR_EACH_POP (mb, hmap_node, map) {
+ destroy_lport_addresses(&mb->laddr);
free(mb);
}
}
@@ -336,6 +348,7 @@ struct mac_cache_stats {
struct ovs_list list_node;
int64_t idle_age_ms;
+ uint64_t packet_count;
union {
/* Common data to identify MAC binding. */
@@ -399,7 +412,8 @@ mac_binding_update_log(const char *action,
}
void
-mac_binding_stats_run(struct ovs_list *stats_list, uint64_t *req_delay,
+mac_binding_stats_run(struct rconn *swconn OVS_UNUSED,
+ struct ovs_list *stats_list, uint64_t *req_delay,
void *data)
{
struct mac_cache_data *cache_data = data;
@@ -495,8 +509,9 @@ fdb_update_log(const char *action,
}
void
-fdb_stats_run(struct ovs_list *stats_list, uint64_t *req_delay,
- void *data)
+fdb_stats_run(struct rconn *swconn OVS_UNUSED,
+ struct ovs_list *stats_list,
+ uint64_t *req_delay, void *data)
{
struct mac_cache_data *cache_data = data;
long long timewall_now = time_wall_msec();
@@ -847,3 +862,90 @@ buffered_packets_db_lookup(struct buffered_packets *bp,
struct ds *ip,
eth_addr_from_string(smb->mac, mac);
}
+
+void
+mac_binding_probe_stats_process_flow_stats(
+ struct ovs_list *stats_list,
+ struct ofputil_flow_stats *ofp_stats)
+{
+ struct mac_cache_stats *stats = xmalloc(sizeof *stats);
+
+ stats->idle_age_ms = ofp_stats->idle_age * 1000;
+ stats->packet_count = ofp_stats->packet_count;
+ stats->data.mb = (struct mac_binding_data) {
+ .cookie = ntohll(ofp_stats->cookie),
+ /* The port_key must be zero to match mac_binding_data_from_sbrec. */
+ .port_key = 0,
+ .dp_key = ntohll(ofp_stats->match.flow.metadata),
+ };
+
+ if (ofp_stats->match.flow.regs[0]) {
+ stats->data.mb.ip =
+ in6_addr_mapped_ipv4(htonl(ofp_stats->match.flow.regs[0]));
+ } else {
+ ovs_be128 ip6 = hton128(flow_get_xxreg(&ofp_stats->match.flow, 1));
+ memcpy(&stats->data.mb.ip, &ip6, sizeof stats->data.mb.ip);
+ }
+
+ ovs_list_push_back(stats_list, &stats->list_node);
+}
+
+void
+mac_binding_probe_stats_run(
+ struct rconn *swconn,
+ struct ovs_list *stats_list,
+ uint64_t *req_delay, void *data)
+{
+ struct mac_cache_data *cache_data = data;
+
+ struct mac_cache_stats *stats;
+ LIST_FOR_EACH_POP (stats, list_node, stats_list) {
+ struct mac_binding *mb = mac_binding_find(&cache_data->mac_bindings,
+ &stats->data.mb);
+ if (!mb) {
+ mac_binding_update_log("Probe: not found in the cache:",
+ &stats->data.mb, false, NULL, 0, 0);
+ free(stats);
+ continue;
+ }
+
+ struct mac_cache_threshold *threshold =
+ mac_cache_threshold_find(cache_data, mb->data.dp_key);
+ if (stats->idle_age_ms < threshold->value ||
+ stats->idle_age_ms > 2 * threshold->value) {
+ free(stats);
+ continue;
+ }
+
+ const struct sbrec_mac_binding *sbrec = mb->sbrec;
+ if (!sbrec->datapath) {
+ free(stats);
+ continue;
+ }
+
+ if (!mb->laddr.n_ipv4_addrs && !mb->laddr.n_ipv6_addrs) {
+ free(stats);
+ continue;
+ }
+
+ mac_binding_update_log("Probe: trying to refresh", &mb->data, true,
+ threshold, stats->idle_age_ms,
+ time_wall_msec() - mb->sbrec->timestamp);
+
+ struct in6_addr local = mb->laddr.n_ipv4_addrs
+ ? in6_addr_mapped_ipv4(mb->laddr.ipv4_addrs[0].addr)
+ : mb->laddr.ipv6_addrs[0].addr;
+ send_self_originated_neigh_packet(swconn,
+ sbrec->datapath->tunnel_key,
+ mb->tunnel_key, mb->laddr.ea,
+ &local, &mb->data.ip,
+ OFTABLE_LOCAL_OUTPUT);
+ free(stats);
+ }
+
+ mac_cache_update_req_delay(&cache_data->thresholds, req_delay);
+ *req_delay = *req_delay / 2;
+ if (*req_delay) {
+ VLOG_DBG("MAC probe binding statistics dalay: %"PRIu64, *req_delay);
+ }
+}
diff --git a/controller/mac-cache.h b/controller/mac-cache.h
index 40d7b9c25..eb2ad7b55 100644
--- a/controller/mac-cache.h
+++ b/controller/mac-cache.h
@@ -70,6 +70,10 @@ struct mac_binding {
const struct sbrec_mac_binding *sbrec;
/* User specified timestamp (in ms) */
long long timestamp;
+ /* SB MAC binding logical port address. */
+ struct lport_addresses laddr;
+ /* SB Port binding tunnel key. */
+ int64_t tunnel_key;
};
struct fdb_data {
@@ -147,7 +151,9 @@ mac_binding_data_init(struct mac_binding_data *data,
}
void mac_binding_add(struct hmap *map, struct mac_binding_data mb_data,
- const struct sbrec_mac_binding *, long long timestamp);
+ const struct sbrec_mac_binding *smb,
+ const struct sbrec_port_binding *pb,
+ long long timestamp);
void mac_binding_remove(struct hmap *map, struct mac_binding *mb);
@@ -180,15 +186,17 @@ void
mac_binding_stats_process_flow_stats(struct ovs_list *stats_list,
struct ofputil_flow_stats *ofp_stats);
-void mac_binding_stats_run(struct ovs_list *stats_list, uint64_t *req_delay,
- void *data);
+void mac_binding_stats_run(struct rconn *swconn OVS_UNUSED,
+ struct ovs_list *stats_list,
+ uint64_t *req_delay, void *data);
/* FDB stat processing. */
void fdb_stats_process_flow_stats(struct ovs_list *stats_list,
struct ofputil_flow_stats *ofp_stats);
-void fdb_stats_run(struct ovs_list *stats_list, uint64_t *req_delay,
- void *data);
+void fdb_stats_run(struct rconn *swconn OVS_UNUSED,
+ struct ovs_list *stats_list,
+ uint64_t *req_delay, void *data);
void mac_cache_stats_destroy(struct ovs_list *stats_list);
@@ -221,4 +229,12 @@ bool buffered_packets_ctx_is_ready_to_send(struct buffered_packets_ctx *ctx);
bool buffered_packets_ctx_has_packets(struct buffered_packets_ctx *ctx);
+void mac_binding_probe_stats_process_flow_stats(
+ struct ovs_list *stats_list,
+ struct ofputil_flow_stats *ofp_stats);
+
+void mac_binding_probe_stats_run(struct rconn *swconn,
+ struct ovs_list *stats_list,
+ uint64_t *req_delay, void *data);
+
#endif /* controller/mac-cache.h */
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index 081411cba..5749f316b 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -3054,14 +3054,17 @@ en_lb_data_cleanup(void *data)
static void
mac_binding_add_sb(struct mac_cache_data *data,
- const struct sbrec_mac_binding *smb)
+ const struct sbrec_mac_binding *smb,
+ struct ovsdb_idl_index *sbrec_port_binding_by_name)
{
struct mac_binding_data mb_data;
if (!mac_binding_data_from_sbrec(&mb_data, smb)) {
return;
}
- mac_binding_add(&data->mac_bindings, mb_data, smb, 0);
+ const struct sbrec_port_binding *pb = lport_lookup_by_name(
+ sbrec_port_binding_by_name, smb->logical_port);
+ mac_binding_add(&data->mac_bindings, mb_data, smb, pb, 0);
}
static void
@@ -3113,7 +3116,8 @@ fdb_remove_sb(struct mac_cache_data *data, const struct
sbrec_fdb *sfdb)
static void
mac_cache_mb_handle_for_datapath(struct mac_cache_data *data,
const struct sbrec_datapath_binding *dp,
- struct ovsdb_idl_index *sbrec_mb_by_dp)
+ struct ovsdb_idl_index *sbrec_mb_by_dp,
+ struct ovsdb_idl_index *sbrec_pb_by_name)
{
bool has_threshold = mac_cache_threshold_find(data, dp->tunnel_key);
@@ -3124,7 +3128,7 @@ mac_cache_mb_handle_for_datapath(struct mac_cache_data *data,
const struct sbrec_mac_binding *mb;
SBREC_MAC_BINDING_FOR_EACH_EQUAL (mb, mb_index_row, sbrec_mb_by_dp) {
if (has_threshold) {
- mac_binding_add_sb(data, mb);
+ mac_binding_add_sb(data, mb, sbrec_pb_by_name);
} else {
mac_binding_remove_sb(data, mb);
}
@@ -3185,6 +3189,10 @@ en_mac_cache_run(struct engine_node *node, void *data)
engine_ovsdb_node_get_index(
engine_get_input("SB_fdb", node),
"dp_key");
+ struct ovsdb_idl_index *sbrec_port_binding_by_name =
+ engine_ovsdb_node_get_index(
+ engine_get_input("SB_port_binding", node),
+ "name");
mac_cache_thresholds_clear(cache_data);
mac_bindings_clear(&cache_data->mac_bindings);
@@ -3199,7 +3207,8 @@ en_mac_cache_run(struct engine_node *node, void *data)
mac_cache_threshold_add(cache_data, sbrec_dp);
mac_cache_mb_handle_for_datapath(cache_data, sbrec_dp,
- sbrec_mb_by_dp);
+ sbrec_mb_by_dp,
+ sbrec_port_binding_by_name);
mac_cache_fdb_handle_for_datapath(cache_data, sbrec_dp,
sbrec_fdb_by_dp_key);
}
@@ -3216,6 +3225,10 @@ mac_cache_sb_mac_binding_handler(struct engine_node
*node, void *data)
const struct sbrec_mac_binding_table *mb_table =
EN_OVSDB_GET(engine_get_input("SB_mac_binding", node));
size_t previous_size = hmap_count(&cache_data->mac_bindings);
+ struct ovsdb_idl_index *sbrec_port_binding_by_name =
+ engine_ovsdb_node_get_index(
+ engine_get_input("SB_port_binding", node),
+ "name");
const struct sbrec_mac_binding *sbrec_mb;
SBREC_MAC_BINDING_TABLE_FOR_EACH_TRACKED (sbrec_mb, mb_table) {
@@ -3231,7 +3244,8 @@ mac_cache_sb_mac_binding_handler(struct engine_node
*node, void *data)
if (mac_cache_threshold_find(cache_data,
sbrec_mb->datapath->tunnel_key)) {
- mac_binding_add_sb(cache_data, sbrec_mb);
+ mac_binding_add_sb(cache_data, sbrec_mb,
+ sbrec_port_binding_by_name);
}
}
@@ -3292,6 +3306,10 @@ mac_cache_runtime_data_handler(struct engine_node *node, void *data OVS_UNUSED)
engine_ovsdb_node_get_index(
engine_get_input("SB_fdb", node),
"dp_key");
+ struct ovsdb_idl_index *sbrec_port_binding_by_name =
+ engine_ovsdb_node_get_index(
+ engine_get_input("SB_port_binding", node),
+ "name");
/* There are no tracked data. Fall back to full recompute. */
if (!rt_data->tracked) {
@@ -3312,7 +3330,8 @@ mac_cache_runtime_data_handler(struct engine_node *node,
void *data OVS_UNUSED)
HMAP_FOR_EACH (tdp, node, &rt_data->tracked_dp_bindings) {
mac_cache_mb_handle_for_datapath(cache_data, tdp->dp,
- sbrec_mb_by_dp);
+ sbrec_mb_by_dp,
+ sbrec_port_binding_by_name);
mac_cache_fdb_handle_for_datapath(cache_data, tdp->dp,
sbrec_fdb_by_dp_key);
@@ -3342,6 +3361,10 @@ mac_cache_sb_datapath_binding_handler(struct engine_node
*node, void *data)
engine_ovsdb_node_get_index(
engine_get_input("SB_fdb", node),
"dp_key");
+ struct ovsdb_idl_index *sbrec_port_binding_by_name =
+ engine_ovsdb_node_get_index(
+ engine_get_input("SB_port_binding", node),
+ "name");
size_t previous_mb_size = hmap_count(&cache_data->mac_bindings);
size_t previous_fdb_size = hmap_count(&cache_data->fdbs);
@@ -3365,7 +3388,8 @@ mac_cache_sb_datapath_binding_handler(struct engine_node
*node, void *data)
SBREC_DATAPATH_BINDING_TABLE_FOR_EACH_TRACKED (sbrec_dp, dp_table) {
mac_cache_mb_handle_for_datapath(cache_data, sbrec_dp,
- sbrec_mb_by_dp);
+ sbrec_mb_by_dp,
+ sbrec_port_binding_by_name);
mac_cache_fdb_handle_for_datapath(cache_data, sbrec_dp,
sbrec_fdb_by_dp_key);
diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index 47c4bf78b..1957ff771 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -4718,7 +4718,7 @@ pinctrl_handle_put_mac_binding(const struct flow *md,
? random_range(MAX_MAC_BINDING_DELAY_MSEC) + 1
: 0;
long long timestamp = time_msec() + delay;
- mac_binding_add(&put_mac_bindings, mb_data, NULL, timestamp);
+ mac_binding_add(&put_mac_bindings, mb_data, NULL, NULL, timestamp);
/* We can send the buffered packet once the main ovn-controller
* thread calls pinctrl_run() and it writes the mac_bindings stored
@@ -4924,7 +4924,7 @@ run_buffered_binding(const struct sbrec_mac_binding_table
*mac_binding_table,
continue;
}
- mac_binding_add(&recent_mbs, mb_data, smb, 0);
+ mac_binding_add(&recent_mbs, mb_data, smb, pb, 0);
const char *redirect_port =
smap_get(&pb->options, "chassis-redirect-port");
@@ -4941,7 +4941,7 @@ run_buffered_binding(const struct sbrec_mac_binding_table
*mac_binding_table,
/* Add the same entry also for chassisredirect port as the buffered
* traffic might be buffered on the cr port. */
mb_data.port_key = pb->tunnel_key;
- mac_binding_add(&recent_mbs, mb_data, smb, 0);
+ mac_binding_add(&recent_mbs, mb_data, smb, NULL, 0);
}
buffered_packets_ctx_run(&buffered_packets_ctx, &recent_mbs,
@@ -5303,7 +5303,7 @@ send_garp_rarp_delete(const char *lport)
notify_pinctrl_handler();
}
-static void
+void
send_self_originated_neigh_packet(struct rconn *swconn,
uint32_t dp_key, uint32_t port_key,
struct eth_addr eth,
diff --git a/controller/pinctrl.h b/controller/pinctrl.h
index 8459f4f53..e33d7cbf5 100644
--- a/controller/pinctrl.h
+++ b/controller/pinctrl.h
@@ -31,6 +31,7 @@ struct ovsdb_idl_index;
struct ovsdb_idl_txn;
struct ovsrec_bridge;
struct ovsrec_open_vswitch_table;
+struct rconn;
struct sbrec_chassis;
struct sbrec_dns_table;
struct sbrec_controller_event_table;
@@ -77,4 +78,10 @@ struct activated_port {
void tag_port_as_activated_in_engine(struct activated_port *ap);
struct ovs_list *get_ports_to_activate_in_engine(void);
bool pinctrl_is_port_activated(int64_t dp_key, int64_t port_key);
+void send_self_originated_neigh_packet(struct rconn *swconn,
+ uint32_t dp_key, uint32_t port_key,
+ struct eth_addr eth,
+ struct in6_addr *local,
+ struct in6_addr *target,
+ uint8_t table_id);
#endif /* controller/pinctrl.h */
diff --git a/controller/statctrl.c b/controller/statctrl.c
index d3c70ccba..6052d2884 100644
--- a/controller/statctrl.c
+++ b/controller/statctrl.c
@@ -39,6 +39,7 @@ VLOG_DEFINE_THIS_MODULE(statctrl);
enum stat_type {
STATS_MAC_BINDING = 0,
STATS_FDB,
+ STATS_MAC_BINDING_PROBE,
STATS_MAX,
};
@@ -62,7 +63,8 @@ struct stats_node {
struct ofputil_flow_stats *ofp_stats);
/* Function to process the parsed stats.
* This function runs in main thread locked behind mutex. */
- void (*run)(struct ovs_list *stats_list, uint64_t *req_delay, void *data);
+ void (*run)(struct rconn *swconn, struct ovs_list *stats_list,
+ uint64_t *req_delay, void *data);
};
#define STATS_NODE(NAME, REQUEST, DESTROY, PROCESS, RUN) \
@@ -145,6 +147,18 @@ statctrl_init(void)
STATS_NODE(FDB, fdb_request, mac_cache_stats_destroy,
fdb_stats_process_flow_stats, fdb_stats_run);
+ struct ofputil_flow_stats_request mac_binding_probe_request = {
+ .cookie = htonll(0),
+ .cookie_mask = htonll(0),
+ .out_port = OFPP_ANY,
+ .out_group = OFPG_ANY,
+ .table_id = OFTABLE_MAC_BINDING,
+ };
+ STATS_NODE(MAC_BINDING_PROBE, mac_binding_probe_request,
+ mac_cache_stats_destroy,
+ mac_binding_probe_stats_process_flow_stats,
+ mac_binding_probe_stats_run);
+
statctrl_ctx.thread = ovs_thread_create("ovn_statctrl",
statctrl_thread_handler,
&statctrl_ctx);
@@ -158,7 +172,11 @@ statctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
return;
}
- void *node_data[STATS_MAX] = {mac_cache_data, mac_cache_data};
+ void *node_data[STATS_MAX] = {
+ mac_cache_data,
+ mac_cache_data,
+ mac_cache_data
+ };
bool schedule_updated = false;
long long now = time_msec();
@@ -168,7 +186,8 @@ statctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
struct stats_node *node = &statctrl_ctx.nodes[i];
uint64_t prev_delay = node->request_delay;
- node->run(&node->stats_list, &node->request_delay, node_data[i]);
+ node->run(statctrl_ctx.swconn, &node->stats_list,
+ &node->request_delay, node_data[i]);
schedule_updated |=
statctrl_update_next_request_timestamp(node, now, prev_delay);
diff --git a/tests/ovn.at b/tests/ovn.at
index 1889f5202..d81f22f3c 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -35862,10 +35862,106 @@ done
uuid2=$(fetch_column mac_binding _uuid ip="192.168.10.10"
logical_port="lr-ls1")
check test "$uuid" = "$uuid2"
-# The other entry must have expired by now.
-check_row_count mac_binding 0 ip="192.168.20.10" logical_port="lr-ls2"
-# Wait for the alive one to expire as well.
-wait_row_count mac_binding 0 ip="192.168.10.10" logical_port="lr-ls1"
+wait_row_count mac_binding 0
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([MAC binding aging - probing])
+AT_SKIP_IF([test $HAVE_SCAPY = no])
+ovn_start
+
+send_udp() {
+ local hv=$1 dev=$2 hdst=$3 hsrc=$4 idst=$5 isrc=$6
+ local packet=$(fmt_pkt "Ether(dst='${hdst}', src='${hsrc}')/ \
+ IP(dst='${idst}', src='${isrc}')/UDP()")
+ as $hv ovs-appctl netdev-dummy/receive $dev $packet
+}
+
+net_add n1
+sim_add hv1
+as hv1
+check ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovn-appctl -t ovn-controller vlog/set mac_cache:file:dbg pinctrl:file:dbg
+
+check ovn-nbctl \
+ -- ls-add ls1 \
+ -- ls-add ls2 \
+ -- lr-add lr \
+ -- set logical_router lr options:mac_binding_age_threshold=15 \
+ -- lrp-add lr lr-ls1 00:00:00:00:10:00 192.168.10.1/24 \
+ -- lrp-add lr lr-ls2 00:00:00:00:20:00 192.168.20.1/24 \
+ -- lsp-add ls1 ls1-lr \
+ -- lsp-set-type ls1-lr router \
+ -- lsp-set-addresses ls1-lr router \
+ -- lsp-set-options ls1-lr router-port=lr-ls1 \
+ -- lsp-add ls1 vif1 \
+ -- lsp-set-addresses vif1 "unknown" \
+ -- lsp-add ls2 ls2-lr \
+ -- lsp-set-type ls2-lr router \
+ -- lsp-set-addresses ls2-lr router \
+ -- lsp-set-options ls2-lr router-port=lr-ls2 \
+ -- lsp-add ls2 vif2 \
+ -- lsp-set-addresses vif2 "unknown"
+
+check ovs-vsctl \
+ -- add-port br-int vif1 \
+ -- set interface vif1 external-ids:iface-id=vif1 \
+ options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap \
+ -- add-port br-int vif2 \
+ -- set interface vif2 external-ids:iface-id=vif2 \
+ options:tx_pcap=hv1/vif2-tx.pcap options:rxq_pcap=hv1/vif2-rx.pcap
+
+OVN_POPULATE_ARP
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+dnl Wait for pinctrl thread to be connected.
+OVS_WAIT_UNTIL([grep pinctrl hv1/ovn-controller.log | grep -q connected])
+
+send_garp hv1 vif1 2 00:00:00:00:10:1a ff:ff:ff:ff:ff:ff 192.168.10.100
192.168.10.100
+wait_row_count mac_binding 1 ip="192.168.10.100" logical_port="lr-ls1"
+
+send_garp hv1 vif2 2 00:00:00:00:10:1b ff:ff:ff:ff:ff:ff 192.168.20.100
192.168.20.100
+wait_row_count mac_binding 1 ip="192.168.20.100" logical_port="lr-ls2"
+
+# Send UDP traffic to create entries in OFTABLE_MAC_BINDING
+send_udp hv1 vif1 00:00:00:00:10:00 00:00:00:00:10:1a 192.168.20.100
192.168.10.100
+echo
'00000000101b00000000200008004500001c000100003f11dbb7c0a80a64c0a814640035003500085f5b'
> expected2