@@ -83,7 +83,8 @@ struct ovn_extend_table;
OVNACT(ND_NS, ovnact_nest) \
OVNACT(SET_METER, ovnact_set_meter) \
OVNACT(OVNFIELD_LOAD, ovnact_load) \
- OVNACT(CHECK_PKT_LARGER, ovnact_check_pkt_larger)
+ OVNACT(CHECK_PKT_LARGER, ovnact_check_pkt_larger)\
+ OVNACT(BIND_VPORT, ovnact_bind_vport)
/* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
enum OVS_PACKED_ENUM ovnact_type {
@@ -318,6 +319,13 @@ struct ovnact_check_pkt_larger {
struct expr_field dst; /* 1-bit destination field. */
};
+/* OVNACT_BIND_VPORT. */
+struct ovnact_bind_vport {
+ struct ovnact ovnact;
+ char *vport;
+ struct expr_field vport_parent; /* Logical virtual port's port name. */
+};
+
/* Internal use by the helpers below. */
void ovnact_init(struct ovnact *, enum ovnact_type, size_t len);
void *ovnact_put(struct ofpbuf *, enum ovnact_type, size_t len);
@@ -486,6 +494,14 @@ enum action_opcode {
* The actions, in OpenFlow 1.3 format, follow the action_header.
*/
ACTION_OPCODE_ICMP4_ERROR,
+
+ /* "bind_vport(vport, vport_parent)".
+ *
+ * 'vport' follows the action_header, in the format - 32-bit field.
+ * 'vport_parent' is passed through the packet metadata as
+ * MFF_LOG_INPORT.
+ */
+ ACTION_OPCODE_BIND_VPORT,
};
/* Header. */
@@ -573,11 +573,31 @@ consider_local_datapath(struct ovsdb_idl_txn *ovnsb_idl_txn,
sbrec_port_binding_set_encap(binding_rec, encap_rec);
}
} else if (binding_rec->chassis == chassis_rec) {
- VLOG_INFO("Releasing lport %s from this chassis.",
- binding_rec->logical_port);
- if (binding_rec->encap)
- sbrec_port_binding_set_encap(binding_rec, NULL);
- sbrec_port_binding_set_chassis(binding_rec, NULL);
+ if (!strcmp(binding_rec->type, "virtual")) {
+ /* pinctrl module takes care of binding the ports
+ * of type 'virtual'.
+ * Release such ports if their virtual parents are no
+ * longer claimed by this chassis. */
+ const struct sbrec_port_binding *parent
+ = lport_lookup_by_name(sbrec_port_binding_by_name,
+ binding_rec->virtual_parent);
+ if (!parent || parent->chassis != chassis_rec) {
+ VLOG_INFO("Releasing lport %s from this chassis.",
+ binding_rec->logical_port);
+ if (binding_rec->encap) {
+ sbrec_port_binding_set_encap(binding_rec, NULL);
+ }
+ sbrec_port_binding_set_chassis(binding_rec, NULL);
+ sbrec_port_binding_set_virtual_parent(binding_rec, NULL);
+ }
+ } else {
+ VLOG_INFO("Releasing lport %s from this chassis.",
+ binding_rec->logical_port);
+ if (binding_rec->encap) {
+ sbrec_port_binding_set_encap(binding_rec, NULL);
+ }
+ sbrec_port_binding_set_chassis(binding_rec, NULL);
+ }
} else if (our_chassis) {
static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
VLOG_INFO_RL(&rl,
@@ -223,8 +223,21 @@ static void send_ipv6_ras(struct rconn *swconn,
OVS_REQUIRES(pinctrl_mutex);
static bool may_inject_pkts(void);
+static void init_put_vport_bindings(void);
+static void destroy_put_vport_bindings(void);
+static void run_put_vport_bindings(
+ struct ovsdb_idl_txn *ovnsb_idl_txn,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+ struct ovsdb_idl_index *sbrec_port_binding_by_key,
+ const struct sbrec_chassis *chassis)
+ OVS_REQUIRES(pinctrl_mutex);
+static void wait_put_vport_bindings(struct ovsdb_idl_txn *ovnsb_idl_txn);
+static void pinctrl_handle_bind_vport(const struct flow *md,
+ struct ofpbuf *userdata);
+
COVERAGE_DEFINE(pinctrl_drop_put_mac_binding);
COVERAGE_DEFINE(pinctrl_drop_buffered_packets_map);
+COVERAGE_DEFINE(pinctrl_drop_put_vport_binding);
void
pinctrl_init(void)
@@ -233,6 +246,7 @@ pinctrl_init(void)
init_send_garps();
init_ipv6_ras();
init_buffered_packets_map();
+ init_put_vport_bindings();
pinctrl.br_int_name = NULL;
pinctrl_handler_seq = seq_create();
pinctrl_main_seq = seq_create();
@@ -1745,6 +1759,12 @@ process_packet_in(struct rconn *swconn, const struct ofp_header *msg)
&pin, &userdata, &continuation);
break;
+ case ACTION_OPCODE_BIND_VPORT:
+ ovs_mutex_lock(&pinctrl_mutex);
+ pinctrl_handle_bind_vport(&pin.flow_metadata.flow, &userdata);
+ ovs_mutex_unlock(&pinctrl_mutex);
+ break;
+
default:
VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
ntohl(ah->opcode));
@@ -1912,6 +1932,8 @@ pinctrl_run(struct ovsdb_idl_txn *ovnsb_idl_txn,
run_put_mac_bindings(ovnsb_idl_txn, sbrec_datapath_binding_by_key,
sbrec_port_binding_by_key,
sbrec_mac_binding_by_lport_ip);
+ run_put_vport_bindings(ovnsb_idl_txn, sbrec_datapath_binding_by_key,
+ sbrec_port_binding_by_key, chassis);
send_garp_prepare(sbrec_port_binding_by_datapath,
sbrec_port_binding_by_name, br_int, chassis,
local_datapaths, active_tunnels);
@@ -2251,6 +2273,7 @@ void
pinctrl_wait(struct ovsdb_idl_txn *ovnsb_idl_txn)
{
wait_put_mac_bindings(ovnsb_idl_txn);
+ wait_put_vport_bindings(ovnsb_idl_txn);
int64_t new_seq = seq_read(pinctrl_main_seq);
seq_wait(pinctrl_main_seq, new_seq);
}
@@ -2267,6 +2290,7 @@ pinctrl_destroy(void)
destroy_ipv6_ras();
destroy_buffered_packets_map();
destroy_put_mac_bindings();
+ destroy_put_vport_bindings();
destroy_dns_cache();
seq_destroy(pinctrl_main_seq);
seq_destroy(pinctrl_handler_seq);
@@ -3290,3 +3314,153 @@ exit:
dp_packet_delete(pkt_out);
}
}
+
+struct put_vport_binding {
+ struct hmap_node hmap_node;
+
+ /* Key and value. */
+ uint32_t dp_key;
+ uint32_t vport_key;
+
+ uint32_t vport_parent_key;
+};
+
+/* Contains "struct put_vport_binding"s. */
+static struct hmap put_vport_bindings;
+
+static void
+init_put_vport_bindings(void)
+{
+ hmap_init(&put_vport_bindings);
+}
+
+static void
+flush_put_vport_bindings(void)
+{
+ struct put_vport_binding *vport_b;
+ HMAP_FOR_EACH_POP (vport_b, hmap_node, &put_vport_bindings) {
+ free(vport_b);
+ }
+}
+
+static void
+destroy_put_vport_bindings(void)
+{
+ flush_put_vport_bindings();
+ hmap_destroy(&put_vport_bindings);
+}
+
+static void
+wait_put_vport_bindings(struct ovsdb_idl_txn *ovnsb_idl_txn)
+{
+ if (ovnsb_idl_txn && !hmap_is_empty(&put_vport_bindings)) {
+ poll_immediate_wake();
+ }
+}
+
+static struct put_vport_binding *
+pinctrl_find_put_vport_binding(uint32_t dp_key, uint32_t vport_key,
+ uint32_t hash)
+{
+ struct put_vport_binding *vpb;
+ HMAP_FOR_EACH_WITH_HASH (vpb, hmap_node, hash, &put_vport_bindings) {
+ if (vpb->dp_key == dp_key && vpb->vport_key == vport_key) {
+ return vpb;
+ }
+ }
+ return NULL;
+}
+
+static void
+run_put_vport_binding(struct ovsdb_idl_txn *ovnsb_idl_txn OVS_UNUSED,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+ struct ovsdb_idl_index *sbrec_port_binding_by_key,
+ const struct sbrec_chassis *chassis,
+ const struct put_vport_binding *vpb)
+{
+ /* Convert logical datapath and logical port key into lport. */
+ const struct sbrec_port_binding *pb = lport_lookup_by_key(
+ sbrec_datapath_binding_by_key, sbrec_port_binding_by_key,
+ vpb->dp_key, vpb->vport_key);
+ if (!pb) {
+ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+
+ VLOG_WARN_RL(&rl, "unknown logical port with datapath %"PRIu32" "
+ "and port %"PRIu32, vpb->dp_key, vpb->vport_key);
+ return;
+ }
+
+ /* pinctrl module updates the port binding only for type 'virtual'. */
+ if (!strcmp(pb->type, "virtual")) {
+ const struct sbrec_port_binding *parent = lport_lookup_by_key(
+ sbrec_datapath_binding_by_key, sbrec_port_binding_by_key,
+ vpb->dp_key, vpb->vport_parent_key);
+ if (parent) {
+ VLOG_INFO("Claiming virtual lport %s for this chassis "
+ "with the virtual parent %s",
+ pb->logical_port, parent->logical_port);
+ sbrec_port_binding_set_chassis(pb, chassis);
+ sbrec_port_binding_set_virtual_parent(pb, parent->logical_port);
+ }
+ }
+}
+
+/* Called by pinctrl_run(). Runs with in the main ovn-controller
+ * thread context. */
+static void
+run_put_vport_bindings(struct ovsdb_idl_txn *ovnsb_idl_txn,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+ struct ovsdb_idl_index *sbrec_port_binding_by_key,
+ const struct sbrec_chassis *chassis)
+ OVS_REQUIRES(pinctrl_mutex)
+{
+ if (!ovnsb_idl_txn) {
+ return;
+ }
+
+ const struct put_vport_binding *vpb;
+ HMAP_FOR_EACH (vpb, hmap_node, &put_vport_bindings) {
+ run_put_vport_binding(ovnsb_idl_txn, sbrec_datapath_binding_by_key,
+ sbrec_port_binding_by_key, chassis, vpb);
+ }
+
+ flush_put_vport_bindings();
+}
+
+/* Called with in the pinctrl_handler thread context. */
+static void
+pinctrl_handle_bind_vport(
+ const struct flow *md, struct ofpbuf *userdata)
+ OVS_REQUIRES(pinctrl_mutex)
+{
+ /* Get the datapath key from the packet metadata. */
+ uint32_t dp_key = ntohll(md->metadata);
+ uint32_t vport_parent_key = md->regs[MFF_LOG_INPORT - MFF_REG0];
+
+ /* Get the virtual port key from the userdata buffer. */
+ uint32_t *vport_key = ofpbuf_try_pull(userdata, sizeof *vport_key);
+
+ if (!vport_key) {
+ return;
+ }
+
+ uint32_t hash = hash_2words(dp_key, *vport_key);
+
+ struct put_vport_binding *vpb
+ = pinctrl_find_put_vport_binding(dp_key, *vport_key, hash);
+ if (!vpb) {
+ if (hmap_count(&put_vport_bindings) >= 1000) {
+ COVERAGE_INC(pinctrl_drop_put_vport_binding);
+ return;
+ }
+
+ vpb = xmalloc(sizeof *vpb);
+ hmap_insert(&put_vport_bindings, &vpb->hmap_node, hash);
+ }
+
+ vpb->dp_key = dp_key;
+ vpb->vport_key = *vport_key;
+ vpb->vport_parent_key = vport_parent_key;
+
+ notify_pinctrl_main();
+}
@@ -2411,6 +2411,64 @@ ovnact_check_pkt_larger_free(struct ovnact_check_pkt_larger *cipl OVS_UNUSED)
{
}
+static void
+parse_bind_vport(struct action_context *ctx)
+{
+ if (!lexer_force_match(ctx->lexer, LEX_T_LPAREN)) {
+ return;
+ }
+
+ if (ctx->lexer->token.type != LEX_T_STRING) {
+ lexer_error(ctx->lexer,
+ "bind_vport requires port name to be specified.");
+ return;
+ }
+
+ struct ovnact_bind_vport *bind_vp = ovnact_put_BIND_VPORT(ctx->ovnacts);
+ bind_vp->vport = xstrdup(ctx->lexer->token.s);
+ lexer_get(ctx->lexer);
+ lexer_force_match(ctx->lexer, LEX_T_COMMA);
+ action_parse_field(ctx, 0, false, &bind_vp->vport_parent);
+ lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+format_BIND_VPORT(const struct ovnact_bind_vport *bind_vp,
+ struct ds *s )
+{
+ ds_put_format(s, "bind_vport(\"%s\", ", bind_vp->vport);
+ expr_field_format(&bind_vp->vport_parent, s);
+ ds_put_cstr(s, ");");
+}
+
+static void
+encode_BIND_VPORT(const struct ovnact_bind_vport *vp,
+ const struct ovnact_encode_params *ep,
+ struct ofpbuf *ofpacts)
+{
+ uint32_t vport_key;
+ if (!ep->lookup_port(ep->aux, vp->vport, &vport_key)) {
+ return;
+ }
+
+ const struct arg args[] = {
+ { expr_resolve_field(&vp->vport_parent), MFF_LOG_INPORT },
+ };
+ encode_setup_args(args, ARRAY_SIZE(args), ofpacts);
+ size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_BIND_VPORT,
+ false, NX_CTLR_NO_METER,
+ ofpacts);
+ ofpbuf_put(ofpacts, &vport_key, sizeof(uint32_t));
+ encode_finish_controller_op(oc_offset, ofpacts);
+ encode_restore_args(args, ARRAY_SIZE(args), ofpacts);
+}
+
+static void
+ovnact_bind_vport_free(struct ovnact_bind_vport *bp)
+{
+ free(bp->vport);
+}
+
/* Parses an assignment or exchange or put_dhcp_opts action. */
static void
parse_set_action(struct action_context *ctx)
@@ -2514,6 +2572,8 @@ parse_action(struct action_context *ctx)
parse_LOG(ctx);
} else if (lexer_match_id(ctx->lexer, "set_meter")) {
parse_set_meter_action(ctx);
+ } else if (lexer_match_id(ctx->lexer, "bind_vport")) {
+ parse_bind_vport(ctx);
} else {
lexer_syntax_error(ctx->lexer, "expecting action");
}
@@ -326,6 +326,7 @@ static const char *OVN_NB_LSP_TYPES[] = {
"router",
"vtep",
"external",
+ "virtual",
};
bool
@@ -519,6 +519,35 @@
some additional flow cost for this and the value appears limited.
</li>
+ <li>
+ <p>
+ If inport is of type <code>virtual</code> adds a priority-100
+ logical flow for each <var>P</var> configured in the
+ <ref table="Logical_Switch_Port" column="options:virtual-parents"/>
+ column if <var>P</var> is not the present virtual parent of the
+ logical port with the match
+ </p>
+ <pre>
+<code>inport == <var>P</var> && ((arp.op == 1 && arp.spa == <var>VIP</var> && arp.tpa == <var>VIP</var>) || (arp.op == 2 && arp.spa == <var>VIP</var>))</code>
+ </pre>
+
+ <p>
+ and applies the action
+ </p>
+ <pre>
+<code>bind_vport(<var>V</var>, inport);</code>
+ </pre>
+
+ <p>
+ and advances the packet to the next table.
+ </p>
+
+ <p>
+ Where <var>VIP</var> is the virtual ip configured in the column
+ <ref table="Logical_Switch_Port" column="options:virtual-ip"/>.
+ </p>
+ </li>
+
<li>
<p>
Priority-50 flows that match ARP requests to each known IP address
@@ -541,7 +570,8 @@ output;
<p>
These flows are omitted for logical ports (other than router ports or
- <code>localport</code> ports) that are down.
+ <code>localport</code> ports) that are down and for logical ports of
+ type <code>virtual</code>.
</p>
</li>
@@ -588,7 +618,8 @@ nd_na_router {
<p>
These flows are omitted for logical ports (other than router ports or
- <code>localport</code> ports) that are down.
+ <code>localport</code> ports) that are down and for logical ports of
+ type <code>virtual</code>.
</p>
</li>
@@ -2024,6 +2055,33 @@ next;
<code>eth.dst = <var>E</var>; next;</code>.
</p>
+ <p>
+ For each virtual ip <var>A</var> configured on a logical port
+ of type <code>virtual</code> and its virtual parent set in
+ its corresponding <ref db="OVN_Southbound" table="Port_Binding"/>
+ record and the virtual parent with the Ethernet address <var>E</var>
+ and the virtual ip is reachable via the router port <var>P</var>, a
+ priority-100 flow with match <code>outport === <var>P</var>
+ && reg0 == <var>A</var></code> has actions
+ <code>eth.dst = <var>E</var>; next;</code>.
+ </p>
+
+ <p>
+ For each virtual ip <var>A</var> configured on a logical port
+ of type <code>virtual</code> and its virtual parent <code>not</code>
+ set in its corresponding
+ <ref db="OVN_Southbound" table="Port_Binding"/>
+ record and the virtual ip <var>A</var> is reachable via the
+ router port <var>P</var>, a
+ priority-100 flow with match <code>outport === <var>P</var>
+ && reg0 == <var>A</var></code> has actions
+ <code>eth.dst = <var>00:00:00:00:00:00</var>; next;</code>.
+ This flow is added so that the ARP is always resolved for the
+ virtual ip <var>A</var> by generating ARP request and
+ <code>not</code> consulting the MAC_Binding table as it can have
+ incorrect value for the virtual ip <var>A</var>.
+ </p>
+
<p>
For each IPv6 address <var>A</var> whose host is known to have
Ethernet address <var>E</var> on router port <var>P</var>, a
@@ -4509,96 +4509,153 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
continue;
}
- /*
- * Add ARP/ND reply flows if either the
- * - port is up or
- * - port type is router or
- * - port type is localport
- */
- if (!lsp_is_up(op->nbsp) && strcmp(op->nbsp->type, "router") &&
- strcmp(op->nbsp->type, "localport")) {
- continue;
- }
+ if (!strcmp(op->nbsp->type, "virtual")) {
+ /* Handle
+ * - GARPs for virtual ip which belongs to a logical port
+ * of type 'virtual' and bind that port.
+ *
+ * - ARP reply from the virtual ip which belongs to a logical
+ * port of type 'virtual' and bind that port.
+ * */
+ ovs_be32 ip;
+ const char *virtual_ip = smap_get(&op->nbsp->options,
+ "virtual-ip");
+ const char *virtual_parents = smap_get(&op->nbsp->options,
+ "virtual-parents");
+ if (!virtual_ip || !virtual_parents ||
+ !ip_parse(virtual_ip, &ip)) {
+ continue;
+ }
- if (lsp_is_external(op->nbsp)) {
- continue;
- }
+ char *tokstr = xstrdup(virtual_parents);
+ char *save_ptr = NULL;
+ char *vparent;
+ for (vparent = strtok_r(tokstr, ",", &save_ptr); vparent != NULL;
+ vparent = strtok_r(NULL, ",", &save_ptr)) {
+ if (op->sb && op->sb->virtual_parent &&
+ op->sb->virtual_parent[0] &&
+ !strcmp(op->sb->virtual_parent, vparent)) {
+ /* virtual port is presently claimed by the
+ * ``vparent``. There is no need to add the lflow
+ * to bind the port. */
+ continue;
+ }
+
+ struct ovn_port *vp = ovn_port_find(ports, vparent);
+ if (!vp || vp->od != op->od) {
+ /* vparent name should be valid and it should belong
+ * to the same logical switch. */
+ continue;
+ }
- for (size_t i = 0; i < op->n_lsp_addrs; i++) {
- for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
ds_clear(&match);
- ds_put_format(&match, "arp.tpa == %s && arp.op == 1",
- op->lsp_addrs[i].ipv4_addrs[j].addr_s);
+ ds_put_format(&match, "inport == \"%s\" && "
+ "((arp.op == 1 && arp.spa == %s && "
+ "arp.tpa == %s) || (arp.op == 2 && "
+ "arp.spa == %s))",
+ vparent, virtual_ip, virtual_ip, virtual_ip);
ds_clear(&actions);
ds_put_format(&actions,
- "eth.dst = eth.src; "
- "eth.src = %s; "
- "arp.op = 2; /* ARP reply */ "
- "arp.tha = arp.sha; "
- "arp.sha = %s; "
- "arp.tpa = arp.spa; "
- "arp.spa = %s; "
- "outport = inport; "
- "flags.loopback = 1; "
- "output;",
- op->lsp_addrs[i].ea_s, op->lsp_addrs[i].ea_s,
- op->lsp_addrs[i].ipv4_addrs[j].addr_s);
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+ "bind_vport(%s, inport); "
+ "next;",
+ op->json_key);
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
ds_cstr(&match), ds_cstr(&actions));
+ }
- /* Do not reply to an ARP request from the port that owns the
- * address (otherwise a DHCP client that ARPs to check for a
- * duplicate address will fail). Instead, forward it the usual
- * way.
- *
- * (Another alternative would be to simply drop the packet. If
- * everything is working as it is configured, then this would
- * produce equivalent results, since no one should reply to the
- * request. But ARPing for one's own IP address is intended to
- * detect situations where the network is not working as
- * configured, so dropping the request would frustrate that
- * intent.) */
- ds_put_format(&match, " && inport == %s", op->json_key);
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
- ds_cstr(&match), "next;");
+ free(tokstr);
+ } else {
+ /*
+ * Add ARP/ND reply flows if either the
+ * - port is up or
+ * - port type is router or
+ * - port type is localport
+ */
+ if (!lsp_is_up(op->nbsp) && strcmp(op->nbsp->type, "router") &&
+ strcmp(op->nbsp->type, "localport")) {
+ continue;
}
- /* For ND solicitations, we need to listen for both the
- * unicast IPv6 address and its all-nodes multicast address,
- * but always respond with the unicast IPv6 address. */
- for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
- ds_clear(&match);
- ds_put_format(&match,
- "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
- op->lsp_addrs[i].ipv6_addrs[j].addr_s,
- op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
- op->lsp_addrs[i].ipv6_addrs[j].addr_s);
+ if (lsp_is_external(op->nbsp)) {
+ continue;
+ }
- ds_clear(&actions);
- ds_put_format(&actions,
- "%s { "
+ for (size_t i = 0; i < op->n_lsp_addrs; i++) {
+ for (size_t j = 0; j < op->lsp_addrs[i].n_ipv4_addrs; j++) {
+ ds_clear(&match);
+ ds_put_format(&match, "arp.tpa == %s && arp.op == 1",
+ op->lsp_addrs[i].ipv4_addrs[j].addr_s);
+ ds_clear(&actions);
+ ds_put_format(&actions,
+ "eth.dst = eth.src; "
"eth.src = %s; "
- "ip6.src = %s; "
- "nd.target = %s; "
- "nd.tll = %s; "
+ "arp.op = 2; /* ARP reply */ "
+ "arp.tha = arp.sha; "
+ "arp.sha = %s; "
+ "arp.tpa = arp.spa; "
+ "arp.spa = %s; "
"outport = inport; "
"flags.loopback = 1; "
- "output; "
- "};",
- !strcmp(op->nbsp->type, "router") ?
- "nd_na_router" : "nd_na",
- op->lsp_addrs[i].ea_s,
- op->lsp_addrs[i].ipv6_addrs[j].addr_s,
- op->lsp_addrs[i].ipv6_addrs[j].addr_s,
- op->lsp_addrs[i].ea_s);
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
- ds_cstr(&match), ds_cstr(&actions));
+ "output;",
+ op->lsp_addrs[i].ea_s, op->lsp_addrs[i].ea_s,
+ op->lsp_addrs[i].ipv4_addrs[j].addr_s);
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+ ds_cstr(&match), ds_cstr(&actions));
+
+ /* Do not reply to an ARP request from the port that owns
+ * the address (otherwise a DHCP client that ARPs to check
+ * for a duplicate address will fail). Instead, forward
+ * it the usual way.
+ *
+ * (Another alternative would be to simply drop the packet.
+ * If everything is working as it is configured, then this
+ * would produce equivalent results, since no one should
+ * reply to the request. But ARPing for one's own IP
+ * address is intended to detect situations where the
+ * network is not working as configured, so dropping the
+ * request would frustrate that intent.) */
+ ds_put_format(&match, " && inport == %s", op->json_key);
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+ ds_cstr(&match), "next;");
+ }
- /* Do not reply to a solicitation from the port that owns the
- * address (otherwise DAD detection will fail). */
- ds_put_format(&match, " && inport == %s", op->json_key);
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
- ds_cstr(&match), "next;");
+ /* For ND solicitations, we need to listen for both the
+ * unicast IPv6 address and its all-nodes multicast address,
+ * but always respond with the unicast IPv6 address. */
+ for (size_t j = 0; j < op->lsp_addrs[i].n_ipv6_addrs; j++) {
+ ds_clear(&match);
+ ds_put_format(&match,
+ "nd_ns && ip6.dst == {%s, %s} && nd.target == %s",
+ op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+ op->lsp_addrs[i].ipv6_addrs[j].sn_addr_s,
+ op->lsp_addrs[i].ipv6_addrs[j].addr_s);
+
+ ds_clear(&actions);
+ ds_put_format(&actions,
+ "%s { "
+ "eth.src = %s; "
+ "ip6.src = %s; "
+ "nd.target = %s; "
+ "nd.tll = %s; "
+ "outport = inport; "
+ "flags.loopback = 1; "
+ "output; "
+ "};",
+ !strcmp(op->nbsp->type, "router") ?
+ "nd_na_router" : "nd_na",
+ op->lsp_addrs[i].ea_s,
+ op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+ op->lsp_addrs[i].ipv6_addrs[j].addr_s,
+ op->lsp_addrs[i].ea_s);
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 50,
+ ds_cstr(&match), ds_cstr(&actions));
+
+ /* Do not reply to a solicitation from the port that owns
+ * the address (otherwise DAD detection will fail). */
+ ds_put_format(&match, " && inport == %s", op->json_key);
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_ARP_ND_RSP, 100,
+ ds_cstr(&match), "next;");
+ }
}
}
}
@@ -7080,7 +7137,8 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
100, ds_cstr(&match), ds_cstr(&actions));
}
}
- } else if (op->od->n_router_ports && strcmp(op->nbsp->type, "router")) {
+ } else if (op->od->n_router_ports && strcmp(op->nbsp->type, "router")
+ && strcmp(op->nbsp->type, "virtual")) {
/* This is a logical switch port that backs a VM or a container.
* Extract its addresses. For each of the address, go through all
* the router ports attached to the switch (to which this port
@@ -7157,6 +7215,105 @@ build_lrouter_flows(struct hmap *datapaths, struct hmap *ports,
}
}
}
+ } else if (op->od->n_router_ports && strcmp(op->nbsp->type, "router")
+ && !strcmp(op->nbsp->type, "virtual")) {
+ /* This is a virtual port. Add ARP replies for the virtual ip with
+ * the mac of the present active virtual parent.
+ * If the logical port doesn't have virtual parent set in
+ * Port_Binding table, then add the flow to set eth.dst to
+ * 00:00:00:00:00:00 and advance to next table so that ARP is
+ * resolved by router pipeline using the arp{} action.
+ * The MAC_Binding entry for the virtual ip might be invalid. */
+ ovs_be32 ip;
+
+ const char *vip = smap_get(&op->nbsp->options,
+ "virtual-ip");
+ const char *virtual_parents = smap_get(&op->nbsp->options,
+ "virtual-parents");
+ if (!vip || !virtual_parents ||
+ !ip_parse(vip, &ip) || !op->sb) {
+ continue;
+ }
+
+ if (!op->sb->virtual_parent || !op->sb->virtual_parent[0] ||
+ !op->sb->chassis) {
+ /* The virtual port is not claimed yet. */
+ for (size_t i = 0; i < op->od->n_router_ports; i++) {
+ const char *peer_name = smap_get(
+ &op->od->router_ports[i]->nbsp->options,
+ "router-port");
+ if (!peer_name) {
+ continue;
+ }
+
+ struct ovn_port *peer = ovn_port_find(ports, peer_name);
+ if (!peer || !peer->nbrp) {
+ continue;
+ }
+
+ if (find_lrp_member_ip(peer, vip)) {
+ ds_clear(&match);
+ ds_put_format(&match, "outport == %s && reg0 == %s",
+ peer->json_key, vip);
+
+ ds_clear(&actions);
+ ds_put_format(&actions,
+ "eth.dst = 00:00:00:00:00:00; next;");
+ ovn_lflow_add(lflows, peer->od,
+ S_ROUTER_IN_ARP_RESOLVE, 100,
+ ds_cstr(&match), ds_cstr(&actions));
+ break;
+ }
+ }
+ } else {
+ struct ovn_port *vp =
+ ovn_port_find(ports, op->sb->virtual_parent);
+ if (!vp || !vp->nbsp) {
+ continue;
+ }
+
+ for (size_t i = 0; i < vp->n_lsp_addrs; i++) {
+ bool found_vip_network = false;
+ const char *ea_s = vp->lsp_addrs[i].ea_s;
+ for (size_t j = 0; j < vp->od->n_router_ports; j++) {
+ /* Get the Logical_Router_Port that the
+ * Logical_Switch_Port is connected to, as
+ * 'peer'. */
+ const char *peer_name = smap_get(
+ &vp->od->router_ports[j]->nbsp->options,
+ "router-port");
+ if (!peer_name) {
+ continue;
+ }
+
+ struct ovn_port *peer =
+ ovn_port_find(ports, peer_name);
+ if (!peer || !peer->nbrp) {
+ continue;
+ }
+
+ if (!find_lrp_member_ip(peer, vip)) {
+ continue;
+ }
+
+ ds_clear(&match);
+ ds_put_format(&match, "outport == %s && reg0 == %s",
+ peer->json_key, vip);
+
+ ds_clear(&actions);
+ ds_put_format(&actions, "eth.dst = %s; next;", ea_s);
+ ovn_lflow_add(lflows, peer->od,
+ S_ROUTER_IN_ARP_RESOLVE, 100,
+ ds_cstr(&match), ds_cstr(&actions));
+ found_vip_network = true;
+ break;
+ }
+
+ if (found_vip_network) {
+ break;
+ }
+ }
+ }
} else if (!strcmp(op->nbsp->type, "router")) {
/* This is a logical switch port that connects to a router. */
@@ -8712,6 +8869,8 @@ main(int argc, char *argv[])
&sbrec_port_binding_col_gateway_chassis);
ovsdb_idl_add_column(ovnsb_idl_loop.idl,
&sbrec_port_binding_col_ha_chassis_group);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl,
+ &sbrec_port_binding_col_virtual_parent);
ovsdb_idl_add_column(ovnsb_idl_loop.idl,
&sbrec_gateway_chassis_col_chassis);
ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_gateway_chassis_col_name);
@@ -400,6 +400,31 @@
</li>
</ul>
</dd>
+
+ <dt><code>virtual</code></dt>
+ <dd>
+ <p>
+ Represents a logical port which does not have an OVS
+ port in the integration bridge and has a virtual ip configured
+ in the <ref column="options:virtual-ip"/> column. This virtual ip
+ can move around between the logical ports configured in
+ the <ref column="options:virtual-parents"/> column.
+ </p>
+
+ <p>
+ One of the use case where <code>virtual</code>
+ ports can be used is.
+ </p>
+
+ <ul>
+ <li>
+ The <code>virtual ip</code> represents a load balancer vip
+ and the <code>virtual parents</code> provide load balancer
+ service in an active-standby setup with the active virtual
+ parent owning the <code>virtual ip</code>.
+ </li>
+ </ul>
+ </dd>
</dl>
</column>
</group>
@@ -553,6 +578,26 @@
interface, in bits.
</column>
</group>
+
+ <group title="Virtual port Options">
+ <p>
+ These options apply when <ref column="type"/> is
+ <code>virtual</code>.
+ </p>
+
+ <column name="options" key="virtual-ip">
+ This option represents the virtual IPv4 address.
+ </column>
+
+ <column name="options" key="virtual-parents">
+ This options represents a set of logical port names (with in the same
+ logical switch) which can own the <code>virtual ip</code> configured
+ in the <ref column="options:virtual-ip"/>. All these virtual parents
+ should add the <code>virtual ip</code> in the
+ <ref column="port_security"/> if port security addressed are enabled.
+ </column>
+ </group>
+
</group>
<group title="Containers">
@@ -1,7 +1,7 @@
{
"name": "OVN_Southbound",
- "version": "2.3.0",
- "cksum": "3092285199 17409",
+ "version": "2.4.0",
+ "cksum": "2392204563 17536",
"tables": {
"SB_Global": {
"columns": {
@@ -173,6 +173,8 @@
"minInteger": 1,
"maxInteger": 4095},
"min": 0, "max": 1}},
+ "virtual_parent": {"type": {"key": "string", "min": 0,
+ "max": 1}},
"chassis": {"type": {"key": {"type": "uuid",
"refTable": "Chassis",
"refType": "weak"},
@@ -1968,6 +1968,24 @@ tcp.flags = RST;
<p><b>Prerequisite:</b> <code>tcp</code></p>
</dd>
+
+ <dt><code>bind_vport(<var>V</var>, <var>P</var>);</code></dt>
+ <dd>
+ <p>
+ <b>Parameters</b>: logical port string field <var>V</var>
+ of type <code>virtual</code>, logical port string field
+ <var>P</var>.
+ </p>
+
+ <p>
+ Binds the virtual logical port <var>V</var> and sets the
+ <ref table="Port_Binding" column="chassis"/> column and
+ <ref table="Port_Binding" column="virtual_parent"/> of
+ the table <ref table="Port_Binding"/>.
+ <ref table="Port_Binding" column="virtual_parent"/> is
+ set to <var>P</var>.
+ </p>
+ </dd>
</dl>
</column>
@@ -2431,6 +2449,13 @@ tcp.flags = RST;
the <code>outport</code> will be reset to the value of the
distributed port.
</dd>
+
+ <dt><code>virtual</code></dt>
+ <dd>
+ Represents a logical port with an <code>virtual ip</code>.
+ This <code>virtual ip</code> can be configured on a
+ logical port (which is refered as virtual parent).
+ </dd>
</dl>
</column>
</group>
@@ -2671,6 +2696,27 @@ tcp.flags = RST;
</column>
</group>
+ <group title="Virtual ports">
+ <column name="virtual_parent">
+ <p>
+ This column is set by <code>ovn-controller</code> with one of the
+ value from the
+ <ref table="Logical_Switch_Port" column="options:virtual-parents"
+ db="OVN_Northbound"/> in the OVN_Northbound database's
+ <ref table="Logical_Switch_Port" db="OVN_Northbound"/> table
+ when the OVN action <code>bind_vport</code> is executed.
+ <code>ovn-controller</code> also sets the
+ <ref column="chassis"/> column when it executes this action
+ with its chassis id.
+ </p>
+
+ <p>
+ <code>ovn-controller</code> sets this column only if the
+ <ref column="type"/> is "virtual".
+ </p>
+ </column>
+ </group>
+
<group title="Naming">
<column name="external_ids" key="name">
<p>
@@ -2137,6 +2137,9 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
case OVNACT_CHECK_PKT_LARGER:
break;
+
+ case OVNACT_BIND_VPORT:
+ break;
}
}
ds_destroy(&s);
@@ -1354,6 +1354,15 @@ reg0 = check_pkt_larger(foo);
reg0[0] = check_pkt_larger(foo);
Syntax error at `foo' expecting `;'.
+# bind_vport
+# lsp1's port key is 0x11.
+bind_vport("lsp1", inport);
+ encodes as controller(userdata=00.00.00.0f.00.00.00.00.11.00.00.00)
+
+# lsp2 doesn't exist. So it should be encoded as drop.
+bind_vport("lsp2", inport);
+ encodes as drop
+
# Miscellaneous negative tests.
;
Syntax error at `;'.
@@ -14017,3 +14026,300 @@ ovn-hv4-0
OVN_CLEANUP([hv1], [hv2], [hv3])
AT_CLEANUP
+
+AT_SETUP([ovn -- virtual ports])
+AT_KEYWORDS([virtual ports])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+send_garp() {
+ local hv=$1 inport=$2 eth_src=$3 eth_dst=$4 spa=$5 tpa=$6
+ local request=${eth_dst}${eth_src}08060001080006040001${eth_src}${spa}${eth_dst}${tpa}
+ as hv$hv ovs-appctl netdev-dummy/receive hv${hv}-vif$inport $request
+}
+
+send_arp_reply() {
+ local hv=$1 inport=$2 eth_src=$3 eth_dst=$4 spa=$5 tpa=$6
+ local request=${eth_dst}${eth_src}08060001080006040002${eth_src}${spa}${eth_dst}${tpa}
+ as hv$hv ovs-appctl netdev-dummy/receive hv${hv}-vif$inport $request
+}
+
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-int hv1-vif1 -- \
+ set interface hv1-vif1 external-ids:iface-id=sw0-p1 \
+ options:tx_pcap=hv1/vif1-tx.pcap \
+ options:rxq_pcap=hv1/vif1-rx.pcap \
+ ofport-request=1
+ovs-vsctl -- add-port br-int hv1-vif2 -- \
+ set interface hv1-vif2 external-ids:iface-id=sw0-p3 \
+ options:tx_pcap=hv1/vif2-tx.pcap \
+ options:rxq_pcap=hv1/vif2-rx.pcap \
+ ofport-request=2
+
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+ovs-vsctl -- add-port br-int hv2-vif1 -- \
+ set interface hv2-vif1 external-ids:iface-id=sw0-p2 \
+ options:tx_pcap=hv2/vif1-tx.pcap \
+ options:rxq_pcap=hv2/vif1-rx.pcap \
+ ofport-request=1
+ovs-vsctl -- add-port br-int hv2-vif2 -- \
+ set interface hv2-vif2 external-ids:iface-id=sw1-p1 \
+ options:tx_pcap=hv2/vif2-tx.pcap \
+ options:rxq_pcap=hv2/vif2-rx.pcap \
+ ofport-request=2
+
+ovn-nbctl ls-add sw0
+
+ovn-nbctl lsp-add sw0 sw0-vir
+ovn-nbctl lsp-set-addresses sw0-vir "50:54:00:00:00:10 10.0.0.10"
+ovn-nbctl lsp-set-port-security sw0-vir "50:54:00:00:00:10 10.0.0.10"
+ovn-nbctl lsp-set-type sw0-vir virtual
+ovn-nbctl set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
+ovn-nbctl set logical_switch_port sw0-vir options:virtual-parents=sw0-p1,sw0-p2
+
+ovn-nbctl lsp-add sw0 sw0-p1
+ovn-nbctl lsp-set-addresses sw0-p1 "50:54:00:00:00:03 10.0.0.3"
+ovn-nbctl lsp-set-port-security sw0-p1 "50:54:00:00:00:03 10.0.0.3 10.0.0.10"
+
+ovn-nbctl lsp-add sw0 sw0-p2
+ovn-nbctl lsp-set-addresses sw0-p2 "50:54:00:00:00:04 10.0.0.4"
+ovn-nbctl lsp-set-port-security sw0-p2 "50:54:00:00:00:04 10.0.0.4 10.0.0.10"
+
+ovn-nbctl lsp-add sw0 sw0-p3
+ovn-nbctl lsp-set-addresses sw0-p3 "50:54:00:00:00:05 10.0.0.5"
+ovn-nbctl lsp-set-port-security sw0-p3 "50:54:00:00:00:05 10.0.0.5"
+
+# Create the second logical switch with one port
+ovn-nbctl ls-add sw1
+ovn-nbctl lsp-add sw1 sw1-p1
+ovn-nbctl lsp-set-addresses sw1-p1 "40:54:00:00:00:03 20.0.0.3"
+ovn-nbctl lsp-set-port-security sw1-p1 "40:54:00:00:00:03 20.0.0.3"
+
+# Create a logical router and attach both logical switches
+ovn-nbctl lr-add lr0
+ovn-nbctl lrp-add lr0 lr0-sw0 00:00:00:00:ff:01 10.0.0.1/24
+ovn-nbctl lsp-add sw0 sw0-lr0
+ovn-nbctl lsp-set-type sw0-lr0 router
+ovn-nbctl lsp-set-addresses sw0-lr0 00:00:00:00:ff:01
+ovn-nbctl lsp-set-options sw0-lr0 router-port=lr0-sw0
+
+ovn-nbctl lrp-add lr0 lr0-sw1 00:00:00:00:ff:02 20.0.0.1/24
+ovn-nbctl lsp-add sw1 sw1-lr0
+ovn-nbctl lsp-set-type sw1-lr0 router
+ovn-nbctl lsp-set-addresses sw1-lr0 00:00:00:00:ff:02
+ovn-nbctl lsp-set-options sw1-lr0 router-port=lr0-sw1
+
+OVN_POPULATE_ARP
+ovn-nbctl --wait=hv sync
+
+# Check that logical flows are added for sw0-vir in lsp_in_arp_rsp pipeline
+# with bind_vport action.
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p1" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p2" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=9 (lr_in_arp_resolve ), priority=100 , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
+])
+
+ip_to_hex() {
+ printf "%02x%02x%02x%02x" "$@"
+}
+
+hv1_ch_uuid=`ovn-sbctl --bare --columns _uuid find chassis name="hv1"`
+hv2_ch_uuid=`ovn-sbctl --bare --columns _uuid find chassis name="hv2"`
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = x])
+
+# From sw0-p0 send GARP for 10.0.0.10. hv1 should claim sw0-vir
+# and sw0-p1 should be its virtual_parent.
+eth_src=505400000003
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 10)
+tpa=$(ip_to_hex 10 0 0 10)
+send_garp 1 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x$hv1_ch_uuid], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = xsw0-p1])
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p2" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=9 (lr_in_arp_resolve ), priority=100 , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
+])
+
+# send the garp from sw0-p2 (in hv2). hv2 should claim sw0-vir
+# and sw0-p2 shpuld be its virtual_parent.
+eth_src=505400000004
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 10)
+tpa=$(ip_to_hex 10 0 0 10)
+send_garp 2 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x$hv2_ch_uuid], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = xsw0-p2])
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p1" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=9 (lr_in_arp_resolve ), priority=100 , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
+])
+
+# Now send arp reply from sw0-p1. hv1 should claim sw0-vir
+# and sw0-p1 shpuld be its virtual_parent.
+eth_src=505400000003
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 10)
+tpa=$(ip_to_hex 10 0 0 4)
+send_arp_reply 1 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x$hv1_ch_uuid], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = xsw0-p1])
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p2" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=9 (lr_in_arp_resolve ), priority=100 , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:03; next;)
+])
+
+# Delete hv1-vif1 port. hv1 should release sw0-vir
+# and the logical flows to bind_vport on sw0-p1 should
+# be added back.
+as hv1 ovs-vsctl del-port hv1-vif1
+
+OVS_WAIT_UNTIL([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = x])
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p1" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p2" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=9 (lr_in_arp_resolve ), priority=100 , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 00:00:00:00:00:00; next;)
+])
+
+# Now send arp reply from sw0-p2. hv2 should claim sw0-vir
+# and sw0-p2 shpuld be its virtual_parent.
+eth_src=505400000004
+eth_dst=ffffffffffff
+spa=$(ip_to_hex 10 0 0 10)
+tpa=$(ip_to_hex 10 0 0 3)
+send_arp_reply 2 1 $eth_src $eth_dst $spa $tpa
+
+OVS_WAIT_UNTIL([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x$hv2_ch_uuid], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = xsw0-p2])
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p1" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=9 (lr_in_arp_resolve ), priority=100 , match=(outport == "lr0-sw0" && reg0 == 10.0.0.10), action=(eth.dst = 50:54:00:00:00:04; next;)
+])
+
+# Delete sw0-p2 logical port
+ovn-nbctl lsp-del sw0-p2
+
+OVS_WAIT_UNTIL([test x$(ovn-sbctl --bare --columns chassis find port_binding \
+logical_port=sw0-vir) = x], [0], [])
+
+AT_CHECK([test x$(ovn-sbctl --bare --columns virtual_parent find port_binding \
+logical_port=sw0-vir) = x])
+
+# Clear virtual_ip column of sw0-vir. There should be no bind_vport flows.
+ovn-nbctl --wait=hv remove logical_switch_port sw0-vir options virtual-ip
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+])
+
+# Add back virtual_ip and clear virtual_parents.
+ovn-nbctl --wait=hv set logical_switch_port sw0-vir options:virtual-ip=10.0.0.10
+
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+ table=11(ls_in_arp_rsp ), priority=100 , match=(inport == "sw0-p1" && ((arp.op == 1 && arp.spa == 10.0.0.10 && arp.tpa == 10.0.0.10) || (arp.op == 2 && arp.spa == 10.0.0.10))), action=(bind_vport("sw0-vir", inport); next;)
+])
+
+ovn-nbctl --wait=hv remove logical_switch_port sw0-vir options virtual-parents
+ovn-sbctl dump-flows sw0 | grep ls_in_arp_rsp | grep bind_vport > lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+])
+
+ovn-sbctl dump-flows lr0 | grep lr_in_arp_resolve | grep "reg0 == 10.0.0.10" \
+> lflows.txt
+
+AT_CHECK([cat lflows.txt], [0], [dnl
+])
+
+OVN_CLEANUP([hv1], [hv2])
+AT_CLEANUP
@@ -1248,6 +1248,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
simap_put(&ports, "eth0", 5);
simap_put(&ports, "eth1", 6);
simap_put(&ports, "LOCAL", ofp_to_u16(OFPP_LOCAL));
+ simap_put(&ports, "lsp1", 0x11);
ds_init(&input);
while (!ds_get_test_line(&input, stdin)) {