@@ -471,13 +471,26 @@ consider_local_datapath(struct ovsdb_idl_txn *ovnsb_idl_txn,
* for them. */
sset_add(local_lports, binding_rec->logical_port);
our_chassis = false;
+ } else if (!strcmp(binding_rec->type, "external")) {
+ const char *chassis_id = smap_get(&binding_rec->options,
+ "requested-chassis");
+ our_chassis = chassis_id && (
+ !strcmp(chassis_id, chassis_rec->name) ||
+ !strcmp(chassis_id, chassis_rec->hostname));
+ if (our_chassis) {
+ add_local_datapath(sbrec_datapath_binding_by_key,
+ sbrec_port_binding_by_datapath,
+ sbrec_port_binding_by_name,
+ binding_rec->datapath, true, local_datapaths);
+ }
}
if (our_chassis
|| !strcmp(binding_rec->type, "patch")
|| !strcmp(binding_rec->type, "localport")
|| !strcmp(binding_rec->type, "vtep")
- || !strcmp(binding_rec->type, "localnet")) {
+ || !strcmp(binding_rec->type, "localnet")
+ || !strcmp(binding_rec->type, "external")) {
update_local_lport_ids(local_lport_ids, binding_rec);
}
@@ -52,7 +52,10 @@ lflow_init(void)
struct lookup_port_aux {
struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath;
struct ovsdb_idl_index *sbrec_port_binding_by_name;
+ struct ovsdb_idl_index *sbrec_port_binding_by_type;
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key;
const struct sbrec_datapath_binding *dp;
+ const struct sbrec_chassis *chassis;
};
struct condition_aux {
@@ -66,6 +69,8 @@ static void consider_logical_flow(
struct ovsdb_idl_index *sbrec_chassis_by_name,
struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath,
struct ovsdb_idl_index *sbrec_port_binding_by_name,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
const struct sbrec_logical_flow *,
const struct hmap *local_datapaths,
const struct sbrec_chassis *,
@@ -89,8 +94,24 @@ lookup_port_cb(const void *aux_, const char *port_name, unsigned int *portp)
const struct sbrec_port_binding *pb
= lport_lookup_by_name(aux->sbrec_port_binding_by_name, port_name);
if (pb && pb->datapath == aux->dp) {
- *portp = pb->tunnel_key;
- return true;
+ if (strcmp(pb->type, "external")) {
+ *portp = pb->tunnel_key;
+ return true;
+ }
+ const char *chassis_id = smap_get(&pb->options,
+ "requested-chassis");
+ if (chassis_id && (!strcmp(chassis_id, aux->chassis->name) ||
+ !strcmp(chassis_id, aux->chassis->hostname))) {
+ const struct sbrec_port_binding *localnet_pb
+ = lport_lookup_by_type(aux->sbrec_datapath_binding_by_key,
+ aux->sbrec_port_binding_by_type,
+ aux->dp->tunnel_key, "localnet");
+ if (localnet_pb) {
+ *portp = localnet_pb->tunnel_key;
+ return true;
+ }
+ }
+ return false;
}
const struct sbrec_multicast_group *mg = mcgroup_lookup_by_dp_name(
@@ -144,6 +165,8 @@ add_logical_flows(
struct ovsdb_idl_index *sbrec_chassis_by_name,
struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath,
struct ovsdb_idl_index *sbrec_port_binding_by_name,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
const struct sbrec_dhcp_options_table *dhcp_options_table,
const struct sbrec_dhcpv6_options_table *dhcpv6_options_table,
const struct sbrec_logical_flow_table *logical_flow_table,
@@ -183,6 +206,8 @@ add_logical_flows(
consider_logical_flow(sbrec_chassis_by_name,
sbrec_multicast_group_by_name_datapath,
sbrec_port_binding_by_name,
+ sbrec_port_binding_by_type,
+ sbrec_datapath_binding_by_key,
lflow, local_datapaths,
chassis, &dhcp_opts, &dhcpv6_opts, &nd_ra_opts,
addr_sets, port_groups, active_tunnels,
@@ -200,6 +225,8 @@ consider_logical_flow(
struct ovsdb_idl_index *sbrec_chassis_by_name,
struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath,
struct ovsdb_idl_index *sbrec_port_binding_by_name,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
const struct sbrec_logical_flow *lflow,
const struct hmap *local_datapaths,
const struct sbrec_chassis *chassis,
@@ -292,7 +319,10 @@ consider_logical_flow(
.sbrec_multicast_group_by_name_datapath
= sbrec_multicast_group_by_name_datapath,
.sbrec_port_binding_by_name = sbrec_port_binding_by_name,
- .dp = lflow->logical_datapath
+ .sbrec_port_binding_by_type = sbrec_port_binding_by_type,
+ .sbrec_datapath_binding_by_key = sbrec_datapath_binding_by_key,
+ .dp = lflow->logical_datapath,
+ .chassis = chassis
};
struct condition_aux cond_aux = {
.sbrec_chassis_by_name = sbrec_chassis_by_name,
@@ -463,6 +493,8 @@ void
lflow_run(struct ovsdb_idl_index *sbrec_chassis_by_name,
struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath,
struct ovsdb_idl_index *sbrec_port_binding_by_name,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
const struct sbrec_dhcp_options_table *dhcp_options_table,
const struct sbrec_dhcpv6_options_table *dhcpv6_options_table,
const struct sbrec_logical_flow_table *logical_flow_table,
@@ -481,7 +513,8 @@ lflow_run(struct ovsdb_idl_index *sbrec_chassis_by_name,
add_logical_flows(sbrec_chassis_by_name,
sbrec_multicast_group_by_name_datapath,
- sbrec_port_binding_by_name, dhcp_options_table,
+ sbrec_port_binding_by_name, sbrec_port_binding_by_type,
+ sbrec_datapath_binding_by_key, dhcp_options_table,
dhcpv6_options_table, logical_flow_table,
local_datapaths, chassis, addr_sets, port_groups,
active_tunnels, local_lport_ids, flow_table, group_table,
@@ -68,6 +68,8 @@ void lflow_init(void);
void lflow_run(struct ovsdb_idl_index *sbrec_chassis_by_name,
struct ovsdb_idl_index *sbrec_multicast_group_by_name_datapath,
struct ovsdb_idl_index *sbrec_port_binding_by_name,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
const struct sbrec_dhcp_options_table *,
const struct sbrec_dhcpv6_options_table *,
const struct sbrec_logical_flow_table *,
@@ -64,6 +64,32 @@ lport_lookup_by_key(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
return retval;
}
+const struct sbrec_port_binding *
+lport_lookup_by_type(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ uint64_t dp_key, const char *port_type)
+{
+ /* Lookup datapath corresponding to dp_key. */
+ const struct sbrec_datapath_binding *db = datapath_lookup_by_key(
+ sbrec_datapath_binding_by_key, dp_key);
+ if (!db) {
+ return NULL;
+ }
+
+ /* Build key for an indexed lookup. */
+ struct sbrec_port_binding *pb = sbrec_port_binding_index_init_row(
+ sbrec_port_binding_by_type);
+ sbrec_port_binding_index_set_datapath(pb, db);
+ sbrec_port_binding_index_set_type(pb, port_type);
+
+ const struct sbrec_port_binding *retval = sbrec_port_binding_index_find(
+ sbrec_port_binding_by_type, pb);
+
+ sbrec_port_binding_index_destroy_row(pb);
+
+ return retval;
+}
+
const struct sbrec_datapath_binding *
datapath_lookup_by_key(struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
uint64_t dp_key)
@@ -42,6 +42,11 @@ const struct sbrec_port_binding *lport_lookup_by_key(
struct ovsdb_idl_index *sbrec_port_binding_by_key,
uint64_t dp_key, uint64_t port_key);
+const struct sbrec_port_binding *lport_lookup_by_type(
+ struct ovsdb_idl_index *sbrec_datapath_binding_by_key,
+ struct ovsdb_idl_index *sbrec_port_binding_by_type,
+ uint64_t dp_key, const char *port_type);
+
const struct sbrec_datapath_binding *datapath_lookup_by_key(
struct ovsdb_idl_index *sbrec_datapath_binding_by_key, uint64_t dp_key);
@@ -145,6 +145,7 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
* ports that have a Gateway_Chassis that point's to our own
* chassis */
sbrec_port_binding_add_clause_type(&pb, OVSDB_F_EQ, "chassisredirect");
+ sbrec_port_binding_add_clause_type(&pb, OVSDB_F_EQ, "external");
if (chassis) {
/* This should be mostly redundant with the other clauses for port
* bindings, but it allows us to catch any ports that are assigned to
@@ -616,6 +617,9 @@ main(int argc, char *argv[])
struct ovsdb_idl_index *sbrec_port_binding_by_datapath
= ovsdb_idl_index_create1(ovnsb_idl_loop.idl,
&sbrec_port_binding_col_datapath);
+ struct ovsdb_idl_index *sbrec_port_binding_by_type
+ = ovsdb_idl_index_create1(ovnsb_idl_loop.idl,
+ &sbrec_port_binding_col_type);
struct ovsdb_idl_index *sbrec_datapath_binding_by_key
= ovsdb_idl_index_create1(ovnsb_idl_loop.idl,
&sbrec_datapath_binding_col_tunnel_key);
@@ -742,6 +746,8 @@ main(int argc, char *argv[])
sbrec_chassis_by_name,
sbrec_multicast_group_by_name_datapath,
sbrec_port_binding_by_name,
+ sbrec_port_binding_by_type,
+ sbrec_datapath_binding_by_key,
sbrec_dhcp_options_table_get(ovnsb_idl_loop.idl),
sbrec_dhcpv6_options_table_get(ovnsb_idl_loop.idl),
sbrec_logical_flow_table_get(ovnsb_idl_loop.idl),
@@ -311,6 +311,7 @@ static const char *OVN_NB_LSP_TYPES[] = {
"localport",
"router",
"vtep",
+ "external",
};
bool
@@ -113,6 +113,26 @@
logical ports on which port security is not enabled, these advance all
packets that match the <code>inport</code>.
</li>
+
+ <li>
+ <p>
+ For each logical port of type <code>external</code> with port
+ security enabled and the logical switch to which it belongs, has a
+ localnet port, a priority 90 flow is added which matches on the
+ <code>inport</code> of localnet port and the valid
+ <code>eth.src</code> address(es) of the <code>external</code>
+ logical port and sets the <code>REGBIT_EXT_PORT_NOT_RESIDENT</code>
+ flag if the logical port doesn't reside on a chassis and advances the
+ packet to the next flow table.
+ </p>
+
+ <p>
+ On the chassis where the <code>external</code> port resides doesn't
+ add the above flow. The priority 50 flow with the match on the
+ <code>inport</code> of the localnet port takes care of forwarding
+ the packet to the next flow table.
+ </p>
+ </li>
</ul>
<p>
@@ -626,7 +646,8 @@ nd_na_router {
<p>
This table adds the DHCPv4 options to a DHCPv4 packet from the
logical ports configured with IPv4 address(es) and DHCPv4 options,
- and similarly for DHCPv6 options.
+ and similarly for DHCPv6 options. This table also adds flows for the
+ logical ports of type <code>external</code>.
</p>
<ul>
@@ -827,7 +848,34 @@ output;
</li>
</ul>
- <h3>Ingress Table 16 Destination Lookup</h3>
+ <h3>Ingress table 16 External ports</h3>
+
+ <p>
+ Traffic from the <code>external</code> logical ports enter the ingress
+ datapath pipeline via the <code>localnet</code> port. This table adds the
+ below logical flows to handle the traffic from these ports.
+ </p>
+
+ <ul>
+ <li>
+ <p>
+ For each router port IP address <code>A</code> which belongs to the
+ logical switch, A priority-100 flow is added which matches
+ <code>REGBIT_EXT_PORT_NOT_RESIDENT && arp.tpa == <var>A</var>
+ && arp.op == 1</code> (ARP request to the router
+ IP) with the action to <code>drop</code> the packet.
+ </p>
+
+ <p>
+ These flows guarantees that the ARP/NS request to the router IP
+ address from the external ports is responded by only the chassis
+ which has claimed these external ports. All the other chassis,
+ drops these packets.
+ </p>
+ </li>
+ </ul>
+
+ <h3>Ingress Table 17 Destination Lookup</h3>
<p>
This table implements switching behavior. It contains these logical
@@ -118,7 +118,8 @@ enum ovn_stage {
PIPELINE_STAGE(SWITCH, IN, DHCP_RESPONSE, 13, "ls_in_dhcp_response") \
PIPELINE_STAGE(SWITCH, IN, DNS_LOOKUP, 14, "ls_in_dns_lookup") \
PIPELINE_STAGE(SWITCH, IN, DNS_RESPONSE, 15, "ls_in_dns_response") \
- PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 16, "ls_in_l2_lkup") \
+ PIPELINE_STAGE(SWITCH, IN, EXTERNAL_PORT, 16, "ls_in_external_port") \
+ PIPELINE_STAGE(SWITCH, IN, L2_LKUP, 17, "ls_in_l2_lkup") \
\
/* Logical switch egress stages. */ \
PIPELINE_STAGE(SWITCH, OUT, PRE_LB, 0, "ls_out_pre_lb") \
@@ -165,12 +166,13 @@ enum ovn_stage {
#define OVN_ACL_PRI_OFFSET 1000
/* Register definitions specific to switches. */
-#define REGBIT_CONNTRACK_DEFRAG "reg0[0]"
-#define REGBIT_CONNTRACK_COMMIT "reg0[1]"
-#define REGBIT_CONNTRACK_NAT "reg0[2]"
-#define REGBIT_DHCP_OPTS_RESULT "reg0[3]"
-#define REGBIT_DNS_LOOKUP_RESULT "reg0[4]"
-#define REGBIT_ND_RA_OPTS_RESULT "reg0[5]"
+#define REGBIT_CONNTRACK_DEFRAG "reg0[0]"
+#define REGBIT_CONNTRACK_COMMIT "reg0[1]"
+#define REGBIT_CONNTRACK_NAT "reg0[2]"
+#define REGBIT_DHCP_OPTS_RESULT "reg0[3]"
+#define REGBIT_DNS_LOOKUP_RESULT "reg0[4]"
+#define REGBIT_ND_RA_OPTS_RESULT "reg0[5]"
+#define REGBIT_EXT_PORT_NOT_RESIDENT "reg0[6]"
/* Register definitions for switches and routers. */
#define REGBIT_NAT_REDIRECT "reg9[0]"
@@ -442,6 +444,8 @@ struct ovn_datapath {
/* Port groups related to the datapath, used only when nbs is NOT NULL. */
struct hmap nb_pgs;
+
+ bool has_external_ports;
};
struct macam_node {
@@ -1581,6 +1585,10 @@ join_logical_ports(struct northd_context *ctx,
od->localnet_port = op;
}
+ if (!strcmp(nbsp->type, "external")) {
+ od->has_external_ports = true;
+ }
+
op->lsp_addrs
= xmalloc(sizeof *op->lsp_addrs * nbsp->n_addresses);
for (size_t j = 0; j < nbsp->n_addresses; j++) {
@@ -2870,6 +2878,12 @@ lsp_is_up(const struct nbrec_logical_switch_port *lsp)
return !lsp->up || *lsp->up;
}
+static bool
+lsp_is_external(const struct nbrec_logical_switch_port *nbsp)
+{
+ return !strcmp(nbsp->type, "external");
+}
+
static bool
build_dhcpv4_action(struct ovn_port *op, ovs_be32 offer_ip,
struct ds *options_action, struct ds *response_action,
@@ -4051,9 +4065,24 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
continue;
}
+ bool is_external = lsp_is_external(op->nbsp);
+ if (is_external && (!op->od->localnet_port || !op->n_ps_addrs)) {
+ /* If the lsp is external with no port securty addresses then,
+ * we don't need to add any port security rules.
+ * The packets from external ports is received on localnet port
+ * and we allow the traffic from localnet ports.
+ *
+ * We also need to ignore these ports if the logical switch
+ * doesn't have a localnet port.
+ */
+ continue;
+ }
+
ds_clear(&match);
ds_clear(&actions);
- ds_put_format(&match, "inport == %s", op->json_key);
+ ds_put_format(
+ &match, "inport == %s",
+ is_external ? op->od->localnet_port->json_key : op->json_key);
build_port_security_l2("eth.src", op->ps_addrs, op->n_ps_addrs,
&match);
@@ -4061,11 +4090,21 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
if (queue_id) {
ds_put_format(&actions, "set_queue(%s); ", queue_id);
}
+
+ if (is_external) {
+ /* Set REGBIT_EXT_PORT_NOT_RESIDENT bit if the external port
+ * doesn't reside on a chassis. */
+ ds_put_format(&match, " && !is_chassis_resident(%s)",
+ op->json_key);
+ ds_put_cstr(&actions, REGBIT_EXT_PORT_NOT_RESIDENT" = 1; ");
+ }
+
ds_put_cstr(&actions, "next;");
- ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC_L2, 50,
+ ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC_L2,
+ is_external ? 90 : 50,
ds_cstr(&match), ds_cstr(&actions));
- if (op->nbsp->n_port_security) {
+ if (op->nbsp->n_port_security && !is_external) {
build_port_security_ip(P_IN, op, lflows);
build_port_security_nd(op, lflows);
}
@@ -4113,7 +4152,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
* - port type is localport
*/
if (!lsp_is_up(op->nbsp) && strcmp(op->nbsp->type, "router") &&
- strcmp(op->nbsp->type, "localport")) {
+ strcmp(op->nbsp->type, "localport") && lsp_is_external(op->nbsp)) {
continue;
}
@@ -4225,6 +4264,13 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
continue;
}
+ bool is_external = lsp_is_external(op->nbsp);
+ if (is_external && !op->od->localnet_port) {
+ /* If it's an external port and there is no localnet port
+ * ignore it. */
+ 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++) {
struct ds options_action = DS_EMPTY_INITIALIZER;
@@ -4237,8 +4283,8 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
ds_put_format(
&match, "inport == %s && eth.src == %s && "
"ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
- "udp.src == 68 && udp.dst == 67", op->json_key,
- op->lsp_addrs[i].ea_s);
+ "udp.src == 68 && udp.dst == 67",
+ op->json_key, op->lsp_addrs[i].ea_s);
ovn_lflow_add(lflows, op->od, S_SWITCH_IN_DHCP_OPTIONS,
100, ds_cstr(&match),
@@ -4343,7 +4389,8 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
/* Ingress table 12 and 13: DHCP options and response, by default goto
* next. (priority 0).
* Ingress table 14 and 15: DNS lookup and response, by default goto next.
- * (priority 0).*/
+ * (priority 0).
+ * Ingress table 16 - External port handling */
HMAP_FOR_EACH (od, key_node, datapaths) {
if (!od->nbs) {
@@ -4354,9 +4401,47 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
ovn_lflow_add(lflows, od, S_SWITCH_IN_DHCP_RESPONSE, 0, "1", "next;");
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 0, "1", "next;");
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 0, "1", "next;");
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_EXTERNAL_PORT, 0, "1", "next;");
+
+ if (od->has_external_ports) {
+ /* Table 16: External port. Drop ARP request for router ips from
+ * external ports if REGBIT_EXT_PORT_NOT_RESIDENT is set.
+ * This makes the router pipeline to be run only the chassis
+ * binding the external ports. */
+ for (size_t i = 0; i < od->n_router_ports; i++) {
+ struct ovn_port *rp = od->router_ports[i];
+ for (size_t j = 0; j < rp->n_lsp_addrs; j++) {
+ for (size_t k = 0; k < rp->lsp_addrs[j].n_ipv4_addrs;
+ k++) {
+ ds_clear(&match);
+ ds_put_format(
+ &match, REGBIT_EXT_PORT_NOT_RESIDENT""
+ " && arp.tpa == %s && arp.op == 1",
+ rp->lsp_addrs[j].ipv4_addrs[k].addr_s);
+ ds_clear(&actions);
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_EXTERNAL_PORT,
+ 100, ds_cstr(&match), "drop;");
+ }
+ for (size_t k = 0; k < rp->lsp_addrs[j].n_ipv6_addrs;
+ k++) {
+ ds_clear(&match);
+ ds_put_format(
+ &match, REGBIT_EXT_PORT_NOT_RESIDENT""
+ " && nd_ns && ip6.dst == {%s, %s} && "
+ "nd.target == %s",
+ rp->lsp_addrs[j].ipv6_addrs[k].addr_s,
+ rp->lsp_addrs[j].ipv6_addrs[k].sn_addr_s,
+ rp->lsp_addrs[j].ipv6_addrs[k].addr_s);
+ ds_clear(&actions);
+ ovn_lflow_add(lflows, od, S_SWITCH_IN_EXTERNAL_PORT,
+ 100, ds_cstr(&match), "drop;");
+ }
+ }
+ }
+ }
}
- /* Ingress table 16: Destination lookup, broadcast and multicast handling
+ /* Ingress table 17: Destination lookup, broadcast and multicast handling
* (priority 100). */
HMAP_FOR_EACH (op, key_node, ports) {
if (!op->nbsp) {
@@ -4376,9 +4461,9 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
"outport = \""MC_FLOOD"\"; output;");
}
- /* Ingress table 16: Destination lookup, unicast handling (priority 50), */
+ /* Ingress table 17: Destination lookup, unicast handling (priority 50), */
HMAP_FOR_EACH (op, key_node, ports) {
- if (!op->nbsp) {
+ if (!op->nbsp || lsp_is_external(op->nbsp)) {
continue;
}
@@ -4495,7 +4580,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
}
}
- /* Ingress table 16: Destination lookup for unknown MACs (priority 0). */
+ /* Ingress table 17: Destination lookup for unknown MACs (priority 0). */
HMAP_FOR_EACH (od, key_node, datapaths) {
if (!od->nbs) {
continue;
@@ -4530,7 +4615,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
* Priority 150 rules drop packets to disabled logical ports, so that they
* don't even receive multicast or broadcast packets. */
HMAP_FOR_EACH (op, key_node, ports) {
- if (!op->nbsp) {
+ if (!op->nbsp || lsp_is_external(op->nbsp)) {
continue;
}
@@ -1627,6 +1627,72 @@
</li>
</ol>
+ <h2>Native OVN services for external logical ports</h2>
+
+ <p>
+ To support OVN native services (like DHCP/IPv6 RA/DNS lookup) to the
+ cloud resources which are external, OVN supports <code>external</code>
+ logical ports.
+ </p>
+
+ <p>
+ Below are some of the use cases where <code>external</code> ports can be
+ used.
+ </p>
+
+ <ul>
+ <li>
+ VMs connected to SR-IOV nics - Traffic from these VMs by passes the
+ kernel stack and local <code>ovn-controller</code> do not bind these
+ ports and cannot serve the native services.
+ </li>
+ <li>
+ When CMS supports provisioning baremetal servers.
+ </li>
+ </ul>
+
+ <p>
+ OVN will provide the native services if CMS has done the below
+ configuration in the <dfn>OVN Northbound Database</dfn>.
+ </p>
+
+ <ul>
+ <li>
+ A row is created in <code>Logical_Switch_Port</code>, configuring the
+ <ref column="addresses" table="Logical_Switch_Port" db="OVN_NB"/> column
+ and setting the <ref column="type" table="Logical_Switch_Port"
+ db="OVN_NB"/> to <code>external</code>.
+ </li>
+
+ <li>
+ <ref column="options:requested-chassis" table="Logical_Switch_Port"
+ db="OVN_NB"/> column is configured to a desired chassis.
+ </li>
+
+ <li>
+ The chassis on which this logical port is requested has the
+ <code>ovn-bridge-mappings</code> configured and has proper L2
+ connectivity so that it can receive the DHCP and other related request
+ packets from these external resources.
+ </li>
+
+ <li>
+ The Logical_Switch of this port has a <code>localnet</code> port.
+ </li>
+
+ <li>
+ Native OVN services are enabled by configuring the DHCP and other
+ options like the way it is done for the normal logical ports.
+ </li>
+ </ul>
+
+ <p>
+ OVN doesn't support HA for these <code>external</code> ports. In case
+ the <code>ovn-controller</code> running on the requested chassis goes down,
+ it is the responsiblity of CMS, to reschedule these <code>external</code>
+ ports to other active chassis.
+ </p>
+
<h1>Security</h1>
<h2>Role-Based Access Controls for the Soutbound DB</h2>
@@ -302,6 +302,39 @@
<dd>
A port to a logical switch on a VTEP gateway.
</dd>
+
+ <dt><code>external</code></dt>
+ <dd>
+ <p>
+ Represents a logical port which is external and not having
+ an OVS port in the integration bridge.
+ <code>OVN</code> will never receive any traffic from this port or
+ send any traffic to this port. <code>OVN</code> can support
+ native services like DHCPv4/DHCPv6/DNS for this port.
+ If <ref column="options:requested-chassis"/> is defined,
+ <code>ovn-controller</code> running in that chassis will bind
+ this port to provide these native services. It is expected that
+ this port belong to a bridged logical switch
+ (with a <code>localnet</code> port).
+ </p>
+
+ <p>
+ Below are some of the use cases where <code>external</code>
+ ports can be used.
+ </p>
+
+ <ul>
+ <li>
+ VMs connected to SR-IOV nics - Traffic from these VMs by passes
+ the kernel stack and local <code>ovn-controller</code> do not
+ bind these ports and cannot serve the native services.
+ </li>
+
+ <li>
+ When CMS supports provisioning baremetal servers.
+ </li>
+ </ul>
+ </dd>
</dl>
</column>
</group>
@@ -9428,9 +9428,9 @@ AT_CHECK([as hv2 ovs-ofctl dump-flows br-int table=32 | grep active_backup | gre
sleep 3 # let BFD sessions settle so we get the right flows on the right chassis
# make sure that flows for handling the outside router port reside on gw1
-AT_CHECK([as gw1 ovs-ofctl dump-flows br-int table=24 | grep 00:00:02:01:02:04 | wc -l], [0], [[1
+AT_CHECK([as gw1 ovs-ofctl dump-flows br-int table=25 | grep 00:00:02:01:02:04 | wc -l], [0], [[1
]])
-AT_CHECK([as gw2 ovs-ofctl dump-flows br-int table=24 | grep 00:00:02:01:02:04 | wc -l], [0], [[0
+AT_CHECK([as gw2 ovs-ofctl dump-flows br-int table=25 | grep 00:00:02:01:02:04 | wc -l], [0], [[0
]])
# make sure ARP responder flows for outside router port reside on gw1 too
@@ -9520,9 +9520,9 @@ AT_CHECK([ovs-vsctl --bare --columns bfd find Interface name=ovn-hv1-0],[0],
sleep 3 # let BFD sessions settle so we get the right flows on the right chassis
# make sure that flows for handling the outside router port reside on gw2 now
-AT_CHECK([as gw2 ovs-ofctl dump-flows br-int table=24 | grep 00:00:02:01:02:04 | wc -l], [0], [[1
+AT_CHECK([as gw2 ovs-ofctl dump-flows br-int table=25 | grep 00:00:02:01:02:04 | wc -l], [0], [[1
]])
-AT_CHECK([as gw1 ovs-ofctl dump-flows br-int table=24 | grep 00:00:02:01:02:04 | wc -l], [0], [[0
+AT_CHECK([as gw1 ovs-ofctl dump-flows br-int table=25 | grep 00:00:02:01:02:04 | wc -l], [0], [[0
]])
# disconnect GW2 from the network, GW1 should take over
@@ -9534,9 +9534,9 @@ sleep 4
bfd_dump
# make sure that flows for handling the outside router port reside on gw2 now
-AT_CHECK([as gw1 ovs-ofctl dump-flows br-int table=24 | grep 00:00:02:01:02:04 | wc -l], [0], [[1
+AT_CHECK([as gw1 ovs-ofctl dump-flows br-int table=25 | grep 00:00:02:01:02:04 | wc -l], [0], [[1
]])
-AT_CHECK([as gw2 ovs-ofctl dump-flows br-int table=24 | grep 00:00:02:01:02:04 | wc -l], [0], [[0
+AT_CHECK([as gw2 ovs-ofctl dump-flows br-int table=25 | grep 00:00:02:01:02:04 | wc -l], [0], [[0
]])
# check that the chassis redirect port has been reclaimed by the gw1 chassis
@@ -11376,6 +11376,524 @@ as hv2 start_daemon ovn-controller
OVN_CLEANUP([hv1],[hv2])
AT_CLEANUP
+AT_SETUP([ovn -- external logical port])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+net_add n1
+sim_add hv1
+sim_add hv2
+
+ovn-nbctl ls-add ls1
+ovn-nbctl lsp-add ls1 ls1-lp1 \
+-- lsp-set-addresses ls1-lp1 "f0:00:00:00:00:01 10.0.0.4 ae70::4"
+
+# Add a couple of external logical port
+ovn-nbctl lsp-add ls1 ls1-lp_ext1 \
+-- lsp-set-addresses ls1-lp_ext1 "f0:00:00:00:00:03 10.0.0.6 ae70::6"
+ovn-nbctl lsp-set-port-security ls1-lp_ext1 \
+"f0:00:00:00:00:03 10.0.0.6 ae70::6"
+ovn-nbctl lsp-set-type ls1-lp_ext1 external
+
+ovn-nbctl lsp-add ls1 ls1-lp_ext2 \
+-- lsp-set-addresses ls1-lp_ext2 "f0:00:00:00:00:04 10.0.0.7 ae70::7"
+ovn-nbctl lsp-set-port-security ls1-lp_ext2 \
+"f0:00:00:00:00:04 10.0.0.7 ae70::8"
+ovn-nbctl lsp-set-type ls1-lp_ext2 external
+
+d1="$(ovn-nbctl create DHCP_Options cidr=10.0.0.0/24 \
+options="\"server_id\"=\"10.0.0.1\" \"server_mac\"=\"ff:10:00:00:00:01\" \
+\"lease_time\"=\"3600\" \"router\"=\"10.0.0.1\"")"
+
+d2="$(ovn-nbctl create DHCP_Options cidr="ae70\:\:/64" \
+options="\"server_id\"=\"00:00:00:10:00:01\"")"
+
+ovn-nbctl lsp-set-dhcpv4-options ls1-lp1 ${d1}
+ovn-nbctl lsp-set-dhcpv4-options ls1-lp_ext1 ${d1}
+ovn-nbctl lsp-set-dhcpv4-options ls1-lp_ext2 ${d1}
+
+ovn-nbctl lsp-set-dhcpv6-options ls1-lp1 ${d2}
+ovn-nbctl lsp-set-dhcpv6-options ls1-lp_ext1 ${d2}
+ovn-nbctl lsp-set-dhcpv6-options ls1-lp_ext2 ${d2}
+
+# Create a logical router and connect it to ls1
+ovn-nbctl lr-add lr0
+ovn-nbctl lrp-add lr0 lr0-ls1 a0:10:00:00:00:01 10.0.0.1/24
+ovn-nbctl lsp-add ls1 ls1-lr0
+ovn-nbctl set Logical_Switch_Port ls1-lr0 type=router \
+ options:router-port=lr0-ls1 addresses=router
+
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-phys hv1-ext1 -- \
+ set interface hv1-ext1 options:tx_pcap=hv1/ext1-tx.pcap \
+ options:rxq_pcap=hv1/ext1-rx.pcap \
+ ofport-request=2
+ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+ovs-vsctl -- add-port br-phys hv2-ext2 -- \
+ set interface hv2-ext2 options:tx_pcap=hv2/ext2-tx.pcap \
+ options:rxq_pcap=hv2/ext2-rx.pcap \
+ ofport-request=2
+ovs-vsctl set open . external-ids:ovn-bridge-mappings=phys:br-phys
+
+ovn-sbctl dump-flows > lflows_n.txt
+
+# No DHCPv4/v6 flows for the external port - ls1-lp_ext1 - 10.0.0.6 in hv1 and
+# hv2 as requested-chassis option is not set and no localnet port added to ls1.
+AT_CHECK([ovn-sbctl dump-flows ls1 | grep "offerip = 10.0.0.6" | \
+wc -l], [0], [0
+])
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | wc -l], [0], [0
+])
+
+hv1_uuid=$(ovn-sbctl list chassis hv1 | grep uuid | awk '{print $3}')
+
+# The port_binding row for ls1-lp_ext1 should have empty chassis
+chassis=$(ovn-sbctl list port_binding ls1-lp_ext1 | grep -v gateway | \
+grep -v requested | grep chassis | awk '{print $3}')
+
+AT_CHECK([test $chassis == "[[]]"], [0], [])
+
+# Set the requested-chassis option for ls1-lp_ext1
+ovn-nbctl --wait=hv lsp-set-options ls1-lp_ext1 requested-chassis=hv1
+
+# No DHCPv4/v6 flows for the external port - ls1-lp_ext1 - 10.0.0.6 in hv1 and hv2
+# as no localnet port added to ls1 yet.
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | wc -l], [0], [0
+])
+
+# Add the localnet port to the logical switch ls1
+ovn-nbctl lsp-add ls1 ln-public
+ovn-nbctl lsp-set-addresses ln-public unknown
+ovn-nbctl lsp-set-type ln-public localnet
+ovn-nbctl --wait=hv lsp-set-options ln-public network_name=phys
+
+ln_public_key=$(ovn-sbctl list port_binding ln-public | grep tunnel_key | \
+awk '{print $3}')
+
+# The ls1-lp_ext1 should be bound to hv1
+chassis=$(ovn-sbctl list port_binding ls1-lp_ext1 | grep -v gateway | \
+grep -v requested | grep chassis | awk '{print $3}')
+AT_CHECK([test $chassis == "$hv1_uuid"], [0], [])
+
+# There should be DHCPv4/v6 OF flows for the ls1-lp_ext1 port in hv1
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | grep reg14=0x$ln_public_key | \
+wc -l], [0], [3
+])
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | \
+grep reg14=0x$ln_public_key | wc -l], [0], [1
+])
+
+# There should ne no DHCPv4/v6 flows for ls1-lp_ext1 on hv2
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | wc -l], [0], [0
+])
+
+# No DHCPv4/v6 flows for the external port - ls1-lp_ext2 - 10.0.0.7 in hv1 and
+# hv2 as requested-chassis option is not set.
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.07" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.07" | wc -l], [0], [0
+])
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.07" | wc -l], [0], [0
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.07" | wc -l], [0], [0
+])
+
+as hv1
+ovs-vsctl show
+
+# This shell function sends a DHCP request packet
+# test_dhcp INPORT SRC_MAC DHCP_TYPE OFFER_IP ...
+test_dhcp() {
+ local inport=$1 src_mac=$2 dhcp_type=$3 offer_ip=$4 use_ip=$5
+ shift; shift; shift; shift; shift;
+ if test $use_ip != 0; then
+ src_ip=$1
+ dst_ip=$2
+ shift; shift;
+ else
+ src_ip=`ip_to_hex 0 0 0 0`
+ dst_ip=`ip_to_hex 255 255 255 255`
+ fi
+ local request=ffffffffffff${src_mac}0800451001100000000080110000${src_ip}${dst_ip}
+ # udp header and dhcp header
+ request=${request}0044004300fc0000
+ request=${request}010106006359aa760000000000000000000000000000000000000000${src_mac}
+ # client hardware padding
+ request=${request}00000000000000000000
+ # server hostname
+ request=${request}0000000000000000000000000000000000000000000000000000000000000000
+ request=${request}0000000000000000000000000000000000000000000000000000000000000000
+ # boot file name
+ request=${request}0000000000000000000000000000000000000000000000000000000000000000
+ request=${request}0000000000000000000000000000000000000000000000000000000000000000
+ request=${request}0000000000000000000000000000000000000000000000000000000000000000
+ request=${request}0000000000000000000000000000000000000000000000000000000000000000
+ # dhcp magic cookie
+ request=${request}63825363
+ # dhcp message type
+ request=${request}3501${dhcp_type}ff
+
+ local srv_mac=$1 srv_ip=$2 expected_dhcp_opts=$3
+ # total IP length will be the IP length of the request packet
+ # (which is 272 in our case) + 8 (padding bytes) + (expected_dhcp_opts / 2)
+ ip_len=`expr 280 + ${#expected_dhcp_opts} / 2`
+ udp_len=`expr $ip_len - 20`
+ ip_len=$(printf "%x" $ip_len)
+ udp_len=$(printf "%x" $udp_len)
+ # $ip_len var will be in 3 digits i.e 134. So adding a '0' before $ip_len
+ local reply=${src_mac}${srv_mac}080045100${ip_len}000000008011XXXX${srv_ip}${offer_ip}
+ # udp header and dhcp header.
+ # $udp_len var will be in 3 digits. So adding a '0' before $udp_len
+ reply=${reply}004300440${udp_len}0000020106006359aa760000000000000000
+ # your ip address
+ reply=${reply}${offer_ip}
+ # next server ip address, relay agent ip address, client mac address
+ reply=${reply}0000000000000000${src_mac}
+ # client hardware padding
+ reply=${reply}00000000000000000000
+ # server hostname
+ reply=${reply}0000000000000000000000000000000000000000000000000000000000000000
+ reply=${reply}0000000000000000000000000000000000000000000000000000000000000000
+ # boot file name
+ reply=${reply}0000000000000000000000000000000000000000000000000000000000000000
+ reply=${reply}0000000000000000000000000000000000000000000000000000000000000000
+ reply=${reply}0000000000000000000000000000000000000000000000000000000000000000
+ reply=${reply}0000000000000000000000000000000000000000000000000000000000000000
+ # dhcp magic cookie
+ reply=${reply}63825363
+ # dhcp message type
+ local dhcp_reply_type=02
+ if test $dhcp_type = 03; then
+ dhcp_reply_type=05
+ fi
+ reply=${reply}3501${dhcp_reply_type}${expected_dhcp_opts}00000000ff00000000
+ echo $reply >> ext1_v4.expected
+
+ as hv1 ovs-appctl netdev-dummy/receive hv${inport}-ext${inport} $request
+}
+
+
+trim_zeros() {
+ sed 's/\(00\)\{1,\}$//'
+}
+
+# This shell function sends a DHCPv6 request packet
+# test_dhcpv6 INPORT SRC_MAC SRC_LLA DHCPv6_MSG_TYPE OFFER_IP OUTPORT...
+# The OUTPORTs (zero or more) list the VIFs on which the original DHCPv6
+# packet should be received twice (one from ovn-controller and the other
+# from the "ovs-ofctl monitor br-int resume"
+test_dhcpv6() {
+ local inport=$1 src_mac=$2 src_lla=$3 msg_code=$4 offer_ip=$5
+ local req_pkt_in_expected=$6
+ local request=ffffffffffff${src_mac}86dd00000000002a1101${src_lla}
+ # dst ip ff02::1:2
+ request=${request}ff020000000000000000000000010002
+ # udp header and dhcpv6 header
+ request=${request}02220223002affff${msg_code}010203
+ # Client identifier
+ request=${request}0001000a00030001${src_mac}
+ # IA-NA (Identity Association for Non Temporary Address)
+ request=${request}0003000c0102030400000e1000001518
+ shift; shift; shift; shift; shift;
+
+ local server_mac=000000100001
+ local server_lla=fe80000000000000020000fffe100001
+ local reply_code=07
+ if test $msg_code = 01; then
+ reply_code=02
+ fi
+ local msg_len=54
+ if test $offer_ip = 1; then
+ msg_len=28
+ fi
+ local reply=${src_mac}${server_mac}86dd0000000000${msg_len}1101
+ reply=${reply}${server_lla}${src_lla}
+
+ # udp header and dhcpv6 header
+ reply=${reply}0223022200${msg_len}ffff${reply_code}010203
+ # Client identifier
+ reply=${reply}0001000a00030001${src_mac}
+ # IA-NA
+ if test $offer_ip != 1; then
+ reply=${reply}0003002801020304ffffffffffffffff00050018${offer_ip}
+ reply=${reply}ffffffffffffffff
+ fi
+ # Server identifier
+ reply=${reply}0002000a00030001${server_mac}
+
+ echo $reply | trim_zeros >> ext${inport}_v6.expected
+ # The inport also receives the request packet since it is connected
+ # to the br-phys.
+ #echo $request >> ext${inport}_v6.expected
+
+ as hv1 ovs-appctl netdev-dummy/receive hv${inport}-ext${inport} $request
+}
+
+reset_pcap_file() {
+ local iface=$1
+ local pcap_file=$2
+ ovs-vsctl -- set Interface $iface options:tx_pcap=dummy-tx.pcap \
+options:rxq_pcap=dummy-rx.pcap
+ rm -f ${pcap_file}*.pcap
+ ovs-vsctl -- set Interface $iface options:tx_pcap=${pcap_file}-tx.pcap \
+options:rxq_pcap=${pcap_file}-rx.pcap
+}
+
+ip_to_hex() {
+ printf "%02x%02x%02x%02x" "$@"
+}
+
+AT_CAPTURE_FILE([ofctl_monitor0_hv1.log])
+as hv1 ovs-ofctl monitor br-int resume --detach --no-chdir \
+--pidfile=ovs-ofctl0.pid 2> ofctl_monitor0_hv1.log
+
+AT_CAPTURE_FILE([ofctl_monitor0_hv2.log])
+as hv2 ovs-ofctl monitor br-int resume --detach --no-chdir \
+--pidfile=ovs-ofctl0.pid 2> ofctl_monitor0_hv2.log
+
+# Send DHCPDISCOVER.
+offer_ip=`ip_to_hex 10 0 0 6`
+server_ip=`ip_to_hex 10 0 0 1`
+server_mac=ff1000000001
+expected_dhcp_opts=330400000e100104ffffff0003040a00000136040a000001
+test_dhcp 1 f00000000003 01 $offer_ip 0 $server_mac $server_ip \
+$expected_dhcp_opts
+
+# NXT_RESUMEs should be 1 in hv1.
+OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
+
+# NXT_RESUMEs should be 0 in hv2.
+OVS_WAIT_UNTIL([test 0 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
+
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_v4.packets
+cat ext1_v4.expected | cut -c -48 > expout
+AT_CHECK([cat ext1_v4.packets | cut -c -48], [0], [expout])
+# Skipping the IPv4 checksum.
+cat ext1_v4.expected | cut -c 53- > expout
+AT_CHECK([cat ext1_v4.packets | cut -c 53-], [0], [expout])
+
+# ovs-ofctl also resumes the packets and this causes other ports to receive
+# the DHCP request packet. So reset the pcap files so that its easier to test.
+reset_pcap_file hv1-ext1 hv1/ext1
+rm -f ext1_v4.expected
+rm -f ext1_v4.packets
+
+# Send DHCPv6 request
+src_mac=f00000000003
+src_lla=fe80000000000000f20000fffe000003
+offer_ip=ae700000000000000000000000000006
+test_dhcpv6 1 $src_mac $src_lla 01 $offer_ip
+
+# NXT_RESUMEs should be 2 in hv1.
+OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
+
+# NXT_RESUMEs should be 0 in hv2.
+OVS_WAIT_UNTIL([test 0 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
+
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap | \
+sort > ext1_v6.packets
+cat ext1_v6.expected | cut -c -120 > expout
+AT_CHECK([cat ext1_v6.packets | cut -c -120], [0], [expout])
+# Skipping the UDP checksum
+cat ext1_v6.expected | cut -c 125- > expout
+AT_CHECK([cat ext1_v6.packets | cut -c 125-], [0], [expout])
+
+rm -f ext1_v6.expected
+rm -f ext1_v6.packets
+reset_pcap_file hv1-ext1 hv1/ext1
+
+# Change the requested-chassis option for ls1-lp_ext1 from hv1 to hv2
+ovn-nbctl --wait=hv lsp-set-options ls1-lp_ext1 requested-chassis=hv2
+
+hv2_uuid=$(ovn-sbctl list chassis hv2 | grep uuid | awk '{print $3}')
+
+# The ls1-lp_ext1 should be bound to hv2
+chassis=$(ovn-sbctl list port_binding ls1-lp_ext1 | grep -v gateway | \
+grep -v requested | grep chassis | awk '{print $3}')
+AT_CHECK([test $chassis == "$hv2_uuid"], [0], [])
+
+# There should be OF flows for DHCP4/v6 for the ls1-lp_ext1 port in hv2
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | grep reg14=0x$ln_public_key | \
+wc -l], [0], [3
+])
+AT_CHECK([as hv2 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | \
+grep reg14=0x$ln_public_key | wc -l], [0], [1
+])
+
+# There should ne no DHCPv4/v6 flows for ls1-lp_ext1 on hv1
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep "0a.00.00.06" | wc -l], [0], [0
+])
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep table=20 | \
+grep controller | grep tp_src=546 | grep \
+"ae.70.00.00.00.00.00.00.00.00.00.00.00.00.00.06" | \
+grep reg14=0x$ln_public_key | wc -l], [0], [0
+])
+
+# Send DHCPDISCOVER again for hv1/ext1. The DHCP response should come from
+# hv2 ovn-controller. Due to the test setup, the port hv1/ext1 is also
+# receiving the expected packet.
+offer_ip=`ip_to_hex 10 0 0 6`
+server_ip=`ip_to_hex 10 0 0 1`
+server_mac=ff1000000001
+expected_dhcp_opts=330400000e100104ffffff0003040a00000136040a000001
+test_dhcp 1 f00000000003 01 $offer_ip 0 $server_mac $server_ip \
+$expected_dhcp_opts
+
+# NXT_RESUMEs should be 2 in hv1.
+OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
+
+# NXT_RESUMEs should be 1 in hv2.
+OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
+
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_v4.packets
+cat ext1_v4.expected | cut -c -48 > expout
+AT_CHECK([cat ext1_v4.packets | cut -c -48], [0], [expout])
+# Skipping the IPv4 checksum.
+cat ext1_v4.expected | cut -c 53- > expout
+AT_CHECK([cat ext1_v4.packets | cut -c 53-], [0], [expout])
+
+# ovs-ofctl also resumes the packets and this causes other ports to receive
+# the DHCP request packet. So reset the pcap files so that its easier to test.
+reset_pcap_file hv1-ext1 hv1/ext1
+rm -f ext1_v4.expected
+
+# Send DHCPv6 request again
+src_mac=f00000000003
+src_lla=fe80000000000000f20000fffe000003
+offer_ip=ae700000000000000000000000000006
+test_dhcpv6 1 $src_mac $src_lla 01 $offer_ip 1
+
+# NXT_RESUMEs should be 2 in hv1.
+OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv1.log | grep -c NXT_RESUME`])
+
+# NXT_RESUMEs should be 2 in hv2.
+OVS_WAIT_UNTIL([test 2 = `cat ofctl_monitor0_hv2.log | grep -c NXT_RESUME`])
+
+as hv1
+ovs-vsctl show
+ovs-ofctl dump-flows br-int
+
+as hv2
+ovs-vsctl show
+ovs-ofctl dump-flows br-int
+
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap | \
+sort > ext1_v6.packets
+cat ext1_v6.expected | cut -c -120 > expout
+AT_CHECK([cat ext1_v6.packets | cut -c -120], [0], [expout])
+# Skipping the UDP checksum
+cat ext1_v6.expected | cut -c 125- > expout
+AT_CHECK([cat ext1_v6.packets | cut -c 125-], [0], [expout])
+
+rm -f ext1_v6.expected
+rm -f ext1_v6.packets
+
+as hv1
+ovs-vsctl show
+reset_pcap_file hv1-ext1 hv1/ext1
+reset_pcap_file br-phys_n1 hv1/br-phys_n1
+reset_pcap_file br-phys hv1/br-phys
+
+as hv2
+ovs-vsctl show
+reset_pcap_file hv2-ext2 hv2/ext2
+reset_pcap_file br-phys_n1 hv2/br-phys_n1
+reset_pcap_file br-phys hv2/br-phys
+
+# From ls1-lp_ext1, send ARP request for the router ip. The ARP
+# response should come from the router pipeline of hv2.
+ext1_mac=f00000000003
+router_mac=a01000000001
+ext1_ip=`ip_to_hex 10 0 0 6`
+router_ip=`ip_to_hex 10 0 0 1`
+arp_request=ffffffffffff${ext1_mac}08060001080006040001${ext1_mac}${ext1_ip}000000000000${router_ip}
+
+as hv1 ovs-appctl netdev-dummy/receive hv1-ext1 $arp_request
+expected_response=${src_mac}${router_mac}08060001080006040002${router_mac}${router_ip}${ext1_mac}${ext1_ip}
+echo $expected_response > expout
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_arp_resp
+AT_CHECK([cat ext1_arp_resp], [0], [expout])
+
+# Verify that the response came from hv2
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv2/br-phys_n1-tx.pcap > ext1_arp_resp
+AT_CHECK([cat ext1_arp_resp], [0], [expout])
+
+
+# # Change the requested-chassis option for ls1-lp_ext1 from hv2 to hv1
+ovn-nbctl --wait=hv lsp-set-options ls1-lp_ext1 requested-chassis=hv1
+
+as hv1
+ovs-vsctl show
+reset_pcap_file hv1-ext1 hv1/ext1
+reset_pcap_file br-phys_n1 hv1/br-phys_n1
+reset_pcap_file br-phys hv1/br-phys
+
+as hv2
+ovs-vsctl show
+reset_pcap_file hv2-ext2 hv2/ext2
+reset_pcap_file br-phys_n1 hv2/br-phys_n1
+reset_pcap_file br-phys hv2/br-phys
+
+as hv1 ovs-appctl netdev-dummy/receive hv1-ext1 $arp_request
+
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv1/ext1-tx.pcap > ext1_arp_resp
+AT_CHECK([cat ext1_arp_resp], [0], [expout])
+
+# Verify that the response didn't come from hv2
+$PYTHON "$top_srcdir/utilities/ovs-pcap.in" hv2/br-phys_n1-tx.pcap > ext1_arp_resp
+AT_CHECK([cat ext1_arp_resp], [0], [])
+
+OVN_CLEANUP([hv1],[hv2])
+AT_CLEANUP
+
AT_SETUP([ovn -- ovn-controller restart])
AT_SKIP_IF([test $HAVE_PYTHON = no])
ovn_start