+
+ sset_destroy(&field_set);
+ sset_destroy(¤t_field_set);
+ } else {
+ group->selection_fields = xasprintf("%s",
+ route->ecmp_selection_fields);
+ }
+ }
+
ovs_list_insert(&group->route_list, &er->list_node);
}
@@ -10440,6 +10476,7 @@ ecmp_groups_add(struct hmap *ecmp_groups,
eg->is_src_route = route->is_src_route;
eg->origin = smap_get_def(&route->route->options, "origin", "");
eg->route_table_id = route->route_table_id;
+ eg->selection_fields = NULL;
ovs_list_init(&eg->route_list);
ecmp_groups_add_route(eg, route);
@@ -10542,7 +10579,8 @@ build_route_prefix_s(const struct in6_addr *prefix, unsigned int plen)
static void
build_route_match(const struct ovn_port *op_inport, uint32_t rtb_id,
const char *network_s, int plen, bool is_src_route,
- bool is_ipv4, struct ds *match, uint16_t *priority, int ofs)
+ bool is_ipv4, struct ds *match, uint16_t *priority, int ofs,
+ bool has_protocol_match)
{
const char *dir;
/* The priority here is calculated to implement longest-prefix-match
@@ -10554,6 +10592,10 @@ build_route_match(const struct ovn_port *op_inport,
uint32_t rtb_id,
dir = "dst";
}
+ if (has_protocol_match) {
+ ofs += 1;
+ }
+
*priority = (plen * ROUTE_PRIO_OFFSET_MULTIPLIER) + ofs;
if (op_inport) {
@@ -10740,7 +10782,7 @@ add_ecmp_symmetric_reply_flows(struct lflow_table
*lflows,
static void
build_ecmp_route_flow(struct lflow_table *lflows, struct ovn_datapath *od,
const struct hmap *lr_ports, struct ecmp_groups_node
*eg,
- struct lflow_ref *lflow_ref)
+ struct lflow_ref *lflow_ref, const char *protocol)
{
bool is_ipv4 = IN6_IS_ADDR_V4MAPPED(&eg->prefix);
@@ -10752,22 +10794,71 @@ build_ecmp_route_flow(struct lflow_table *lflows,
struct ovn_datapath *od,
int ofs = !strcmp(eg->origin, ROUTE_ORIGIN_CONNECTED) ?
ROUTE_PRIO_OFFSET_CONNECTED: ROUTE_PRIO_OFFSET_STATIC;
build_route_match(NULL, eg->route_table_id, prefix_s, eg->plen,
- eg->is_src_route, is_ipv4, &route_match, &priority, ofs);
+ eg->is_src_route, is_ipv4, &route_match, &priority, ofs,
+ protocol != NULL);
free(prefix_s);
- struct ds actions = DS_EMPTY_INITIALIZER;
- ds_put_format(&actions, "ip.ttl--; flags.loopback = 1; %s = %"PRIu16
- "; %s = select(", REG_ECMP_GROUP_ID, eg->id,
- REG_ECMP_MEMBER_ID);
+ if (eg->selection_fields && protocol) {
+ if (!strcmp(protocol, "tcp")) {
+ ds_put_format(&route_match, " && tcp");
+ } else if (!strcmp(protocol, "udp")) {
+ ds_put_format(&route_match, " && udp");
+ }
+ }
+ struct ds values = DS_EMPTY_INITIALIZER;
bool is_first = true;
LIST_FOR_EACH (er, list_node, &eg->route_list) {
if (is_first) {
is_first = false;
} else {
- ds_put_cstr(&actions, ", ");
+ ds_put_cstr(&values, ", ");
}
- ds_put_format(&actions, "%"PRIu16, er->id);
+ ds_put_format(&values, "%"PRIu16, er->id);
+ }
+
+ struct ds actions = DS_EMPTY_INITIALIZER;
+ if (eg->selection_fields) {
+ struct sset current_field_set;
+ struct sset field_set = SSET_INITIALIZER(&field_set);
+ sset_from_delimited_string(¤t_field_set,
+ eg->selection_fields, ",");
+
+ const char *field;
+ SSET_FOR_EACH (field, ¤t_field_set) {
+ if (!strcmp(field, "src_port")) {
+ if (protocol) {
+ if (!strcmp(protocol, "tcp")) {
+ sset_add(&field_set, "tcp_src");
+ } else if (!strcmp(protocol, "udp")) {
+ sset_add(&field_set, "udp_src");
+ }
+ }
+ } else if (!strcmp(field, "dst_port")) {
+ if (protocol) {
+ if (!strcmp(protocol, "tcp")) {
+ sset_add(&field_set, "tcp_dst");
+ } else if (!strcmp(protocol, "udp")) {
+ sset_add(&field_set, "udp_dst");
+ }
+ }
+ } else {
+ sset_add(&field_set, field);
+ }
+ }
+
+ ds_put_format(&actions, "ip.ttl--; flags.loopback = 1; %s = %"PRIu16
+ "; %s = select(values=%s", REG_ECMP_GROUP_ID, eg->id,
+ REG_ECMP_MEMBER_ID, ds_cstr(&values));
+ ds_put_format(&actions, "; hash_fields=\"%s\"",
+ sset_join(&field_set, ",", ""));
+
+ sset_destroy(¤t_field_set);
+ sset_destroy(&field_set);
+ } else {
+ ds_put_format(&actions, "ip.ttl--; flags.loopback = 1; %s = %"PRIu16
+ "; %s = select(%s", REG_ECMP_GROUP_ID, eg->id,
+ REG_ECMP_MEMBER_ID, ds_cstr(&values));
}
ds_put_cstr(&actions, ");");
@@ -10847,7 +10938,7 @@ add_route(struct lflow_table *lflows, struct
ovn_datapath *od,
}
}
build_route_match(op_inport, rtb_id, network_s, plen, is_src_route,
- is_ipv4, &match, &priority, ofs);
+ is_ipv4, &match, &priority, ofs, false);
struct ds common_actions = DS_EMPTY_INITIALIZER;
struct ds actions = DS_EMPTY_INITIALIZER;
@@ -12733,7 +12824,13 @@ build_static_route_flows_for_lrouter(
HMAP_FOR_EACH (group, hmap_node, &ecmp_groups) {
/* add a flow in IP_ROUTING, and one flow for each member in
* IP_ROUTING_ECMP. */
- build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref);
+ build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref, NULL);
+ if (group->selection_fields) {
+ build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref,
+ "tcp");
+ build_ecmp_route_flow(lflows, od, lr_ports, group, lflow_ref,
+ "udp");
+ }
}
const struct unique_routes_node *ur;
HMAP_FOR_EACH (ur, hmap_node, &unique_routes) {
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 6376320d3..7576bf7d2 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3718,6 +3718,23 @@ or
</p>
</column>
+ <column name="options" key="ecmp_selection_fields">
+ <p>
+ ECMP routes use OpenFlow groups of type <code>select</code> to
+ pick a nexthop among the list of avaible nexthops.
+ the buckets of a group. Please see the OVS documentation
+ (man ovs-ofctl) for more details on the selection methods.
+ </p>
+
+ <p>
+ OVN by default uses <code>dp_hash</code>. In order to use the
+ <code>hash</code> selection method, specify comma-separated
+ list of selection fields.
+ </p>
+ </column>
+
<column name="options" key="origin">
In case ovn-interconnection has been learned this route, it will have
its origin set: either "connected" or "static". This key is supposed
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 199197f09..3d3046a21 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -6704,9 +6704,9 @@ ovn-sbctl dump-flows lr0 > lr0flows
AT_CHECK([grep -w "lr_in_ip_routing" lr0flows | ovn_strip_lflows], [0], [dnl
table=??(lr_in_ip_routing ), priority=0 , match=(1), action=(drop;)
table=??(lr_in_ip_routing ), priority=10550, match=(nd_rs || nd_ra),
action=(drop;)
- table=??(lr_in_ip_routing ), priority=194 , match=(inport == "lr0-public" &&
ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 =
fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback =
1; next;)
- table=??(lr_in_ip_routing ), priority=74 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=97 , match=(reg7 == 0 && ip4.dst ==
1.0.0.1/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1;
eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=124 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=162 , match=(reg7 == 0 && ip4.dst ==
1.0.0.1/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1;
eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=324 , match=(inport == "lr0-public" &&
ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 =
fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback =
1; next;)
])
AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | ovn_strip_lflows], [0], [dnl
@@ -6721,9 +6721,9 @@ AT_CHECK([grep -w "lr_in_ip_routing" lr0flows |
ovn_strip_lflows], [0], [dnl
table=??(lr_in_ip_routing ), priority=0 , match=(1), action=(drop;)
table=??(lr_in_ip_routing ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 ==
0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13;
reg1 = 192.168.0.1; outport = "lr0-public"; next;)
table=??(lr_in_ip_routing ), priority=10550, match=(nd_rs || nd_ra),
action=(drop;)
- table=??(lr_in_ip_routing ), priority=194 , match=(inport == "lr0-public" &&
ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 =
fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback =
1; next;)
- table=??(lr_in_ip_routing ), priority=74 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=97 , match=(reg7 == 0 && ip4.dst
== 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]]
= select(1, 2);)
+ table=??(lr_in_ip_routing ), priority=124 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=162 , match=(reg7 == 0 && ip4.dst
== 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]]
= select(1, 2);)
+ table=??(lr_in_ip_routing ), priority=324 , match=(inport == "lr0-public" &&
ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 =
fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback =
1; next;)
])
AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed
's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_lflows], [0], [dnl
table=??(lr_in_ip_routing_ecmp), priority=0 , match=(1), action=(drop;)
@@ -6750,9 +6750,9 @@ AT_CHECK([grep -w "lr_in_ip_routing" lr0flows |
ovn_strip_lflows], [0], [dnl
table=??(lr_in_ip_routing ), priority=0 , match=(1), action=(drop;)
table=??(lr_in_ip_routing ), priority=10300, match=(ct_mark.ecmp_reply_port == 1 && reg7 ==
0 && ip4.dst == 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; eth.src = 00:00:20:20:12:13;
reg1 = 192.168.0.1; outport = "lr0-public"; next;)
table=??(lr_in_ip_routing ), priority=10550, match=(nd_rs || nd_ra),
action=(drop;)
- table=??(lr_in_ip_routing ), priority=194 , match=(inport == "lr0-public" &&
ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 =
fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback =
1; next;)
- table=??(lr_in_ip_routing ), priority=74 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=97 , match=(reg7 == 0 && ip4.dst
== 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]]
= select(1, 2);)
+ table=??(lr_in_ip_routing ), priority=124 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=162 , match=(reg7 == 0 && ip4.dst
== 1.0.0.1/32), action=(ip.ttl--; flags.loopback = 1; reg8[[0..15]] = 1; reg8[[16..31]]
= select(1, 2);)
+ table=??(lr_in_ip_routing ), priority=324 , match=(inport == "lr0-public" &&
ip6.dst == fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 =
fe80::200:20ff:fe20:1213; eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback =
1; next;)
])
AT_CHECK([grep -e "lr_in_ip_routing_ecmp" lr0flows | sed
's/192\.168\.0\..0/192.168.0.??/' | ovn_strip_lflows], [0], [dnl
table=??(lr_in_ip_routing_ecmp), priority=0 , match=(1), action=(drop;)
@@ -6768,14 +6768,14 @@ check ovn-nbctl --wait=sb lr-route-add lr0 1.0.0.0/24
192.168.0.10
ovn-sbctl dump-flows lr0 > lr0flows
AT_CHECK([grep -e "lr_in_ip_routing.*192.168.0.10" lr0flows | ovn_strip_lflows], [0], [dnl
- table=??(lr_in_ip_routing ), priority=73 , match=(reg7 == 0 && ip4.dst ==
1.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1;
eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=122 , match=(reg7 == 0 && ip4.dst ==
1.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1;
eth.src = 00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
])
check ovn-nbctl --wait=sb lr-route-add lr0 2.0.0.0/24 lr0-public
ovn-sbctl dump-flows lr0 > lr0flows
AT_CHECK([grep -e "lr_in_ip_routing.*2.0.0.0" lr0flows | ovn_strip_lflows],
[0], [dnl
- table=??(lr_in_ip_routing ), priority=73 , match=(reg7 == 0 && ip4.dst ==
2.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=122 , match=(reg7 == 0 && ip4.dst ==
2.0.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:20:20:12:13; outport = "lr0-public"; flags.loopback = 1; next;)
])
AT_CLEANUP
@@ -7199,16 +7199,16 @@ AT_CHECK([grep "lr_in_ip_routing_pre" lr0flows |
ovn_strip_lflows], [0], [dnl
grep -e "(lr_in_ip_routing ).*outport" lr0flows
AT_CHECK([grep -e "(lr_in_ip_routing ).*outport" lr0flows | ovn_strip_lflows], [0], [dnl
- table=??(lr_in_ip_routing ), priority=1 , match=(reg7 == 0 && ip4.dst ==
0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src
= 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=1 , match=(reg7 == 2 && ip4.dst ==
0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src
= 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=194 , match=(inport == "lrp0" && ip6.dst ==
fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:1; eth.src =
00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=194 , match=(inport == "lrp1" && ip6.dst ==
fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:101; eth.src
= 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=194 , match=(inport == "lrp2" && ip6.dst ==
fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:201; eth.src
= 00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=73 , match=(reg7 == 1 && ip4.dst ==
192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.1.10; reg1 = 192.168.1.1;
eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=74 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=74 , match=(ip4.dst == 192.168.1.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.1.1; eth.src =
00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=74 , match=(ip4.dst == 192.168.2.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.2.1; eth.src =
00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
- table=??(lr_in_ip_routing ), priority=97 , match=(reg7 == 2 && ip4.dst ==
1.1.1.1/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.20; reg1 = 192.168.0.1;
eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=122 , match=(reg7 == 1 && ip4.dst ==
192.168.0.0/24), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.1.10; reg1 = 192.168.1.1;
eth.src = 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=124 , match=(ip4.dst == 192.168.0.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.0.1; eth.src =
00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=124 , match=(ip4.dst == 192.168.1.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.1.1; eth.src =
00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=124 , match=(ip4.dst == 192.168.2.0/24),
action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = ip4.dst; reg1 = 192.168.2.1; eth.src =
00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=162 , match=(reg7 == 2 && ip4.dst ==
1.1.1.1/32), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.20; reg1 = 192.168.0.1;
eth.src = 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=2 , match=(reg7 == 0 && ip4.dst ==
0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src
= 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=2 , match=(reg7 == 2 && ip4.dst ==
0.0.0.0/0), action=(ip.ttl--; reg8[[0..15]] = 0; reg0 = 192.168.0.10; reg1 = 192.168.0.1; eth.src
= 00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=324 , match=(inport == "lrp0" && ip6.dst ==
fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:1; eth.src =
00:00:00:00:00:01; outport = "lrp0"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=324 , match=(inport == "lrp1" && ip6.dst ==
fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:101; eth.src
= 00:00:00:00:01:01; outport = "lrp1"; flags.loopback = 1; next;)
+ table=??(lr_in_ip_routing ), priority=324 , match=(inport == "lrp2" && ip6.dst ==
fe80::/64), action=(ip.ttl--; reg8[[0..15]] = 0; xxreg0 = ip6.dst; xxreg1 = fe80::200:ff:fe00:201; eth.src
= 00:00:00:00:02:01; outport = "lrp2"; flags.loopback = 1; next;)
])
AT_CLEANUP
diff --git a/tests/ovn.at b/tests/ovn.at
index b31afbfb3..f45ffdfb5 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -2086,6 +2086,16 @@ reg0 = select(1, 2);
encodes as group:20
uses group: id(20),
name(type=select,selection_method=dp_hash,bucket=bucket_id=0,weight:100,actions=load:1->xxreg0[[96..127]],resubmit(,oflow_in_table),bucket=bucket_id=1,weight:100,actions=load:2->xxreg0[[96..127]],resubmit(,oflow_in_table))
+reg9[[16..31]] = select(values=1=50, 2=100, 3; hash_fields="ip_src,ip_dst" );
+ formats as reg9[[16..31]] = select(values=1=50, 2=100, 3=100;
hash_fields="ip_src,ip_dst");
+ encodes as group:21
+ uses group: id(21),
name(type=select,selection_method=hash,fields(ip_src,ip_dst),bucket=bucket_id=0,weight:50,actions=load:1->xreg4[[16..31]],resubmit(,oflow_in_table),bucket=bucket_id=1,weight:100,actions=load:2->xreg4[[16..31]],resubmit(,oflow_in_table),bucket=bucket_id=2,weight:100,actions=load:3->xreg4[[16..31]],resubmit(,oflow_in_table))
+
+reg0 = select(values=1, 2; hash_fields="ip_dst,ip_src");
+ formats as reg0 = select(values=1=100, 2=100; hash_fields="ip_dst,ip_src");
+ encodes as group:22
+ uses group: id(22),
name(type=select,selection_method=hash,fields(ip_dst,ip_src),bucket=bucket_id=0,weight:100,actions=load:1->xxreg0[[96..127]],resubmit(,oflow_in_table),bucket=bucket_id=1,weight:100,actions=load:2->xxreg0[[96..127]],resubmit(,oflow_in_table))
+
reg0 = select(1=, 2);
Syntax error at `,' expecting weight.
reg0 = select(1=0, 2);
@@ -2094,6 +2104,14 @@ reg0 = select(1=123456, 2);
Syntax error at `123456' expecting weight.
reg0 = select(123);
Syntax error at `;' expecting at least 2 group members.
+reg0 = select(values=1, 2);
+ Syntax error at `;' expecting hash_fields.
+reg0 = select(values=1, 2; hash_fields);
+ Syntax error at `)' invalid hash_fields.
+reg0 = select(values=1, 2; hash_fields=);
+ Syntax error at `)' invalid hash_fields.
+reg0 = select(1, 2; hash_fields="ip_src");
+ Syntax error at `=' hash_fields unexpected.
ip.proto = select(1, 2, 3);
Field ip.proto is not modifiable.
reg0[[0..14]] = select(1, 2, 3);
@@ -2101,12 +2119,12 @@ reg0[[0..14]] = select(1, 2, 3);
fwd_group(liveness=true, childports="eth0", "lsp1");
formats as fwd_group(liveness="true", childports="eth0", "lsp1");
- encodes as group:21
- uses group: id(21),
name(type=select,selection_method=dp_hash,bucket=watch_port:5,load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=watch_port:17,load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
+ encodes as group:23
+ uses group: id(23),
name(type=select,selection_method=dp_hash,bucket=watch_port:5,load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=watch_port:17,load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
fwd_group(childports="eth0", "lsp1");
- encodes as group:22
- uses group: id(22),
name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
+ encodes as group:24
+ uses group: id(24),
name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
fwd_group(childports=eth0);
Syntax error at `eth0' expecting logical switch port.
@@ -2115,8 +2133,8 @@ fwd_group();
Syntax error at `)' expecting `;'.
fwd_group(childports="eth0", "lsp1");
- encodes as group:22
- uses group: id(22),
name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
+ encodes as group:24
+ uses group: id(24),
name(type=select,selection_method=dp_hash,bucket=load=0x5->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT),bucket=load=0x17->NXM_NX_REG15[[0..15]],resubmit(,OFTABLE_SAVE_INPORT))
fwd_group(liveness=xyzzy, childports="eth0", "lsp1");
Syntax error at `xyzzy' expecting true or false.
@@ -26531,6 +26549,218 @@ OVN_CLEANUP([hv1])
AT_CLEANUP
])
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ECMP static routes - custom hash])
+ovn_start
+
+# Logical network:
+# ls1 (192.168.1.0/24) - lr1 - ls2 (192.168.2.0/24)
+# lsl has lsp11 (192.168.1.11) and ls2 has lsp21 (192.168.2.21) and lsp22
+# (192.168.2.22)
+#
+# Static routes on lr1:
+# 10.0.0.0/24 nexthop 192.168.2.21
+# 10.0.0.0/24 nexthop 192.168.2.22
+#
+# ECMP hash on ip_proto,src_ip,dst_ip,dst_port
+#
+# Test:
+# lsp11 send packets to 10.0.0.100 with different source ports
+#
+# Expected result:
+# All packets should go out of a either lsp21 or lsp22
+
+ovn-nbctl lr-add lr1
+
+ovn-nbctl ls-add ls1
+ovn-nbctl ls-add ls2
+
+for i in 1 2; do
+ ovn-nbctl lrp-add lr1 lrp-lr1-ls${i} 00:00:00:01:0${i}:01 192.168.${i}.1/24
+ ovn-nbctl lsp-add ls${i} lsp-ls${i}-lr1 -- lsp-set-type lsp-ls${i}-lr1
router \
+ -- lsp-set-options lsp-ls${i}-lr1 router-port=lrp-lr1-ls${i} \
+ -- lsp-set-addresses lsp-ls${i}-lr1 router
+done
+
+#install static routes
+ovn-nbctl --ecmp-selection-fields="ip_proto,ip_src" lr-route-add lr1
10.0.0.0/24 192.168.2.21
+ovn-nbctl --ecmp --ecmp-selection-fields="ip_proto,ip_src,ip_dst,dst_port"
lr-route-add lr1 10.0.0.0/24 192.168.2.22
+
+# Create logical ports
+ovn-nbctl lsp-add ls1 lsp11 -- \
+ lsp-set-addresses lsp11 "f0:00:00:00:01:11 192.168.1.11"
+ovn-nbctl lsp-add ls2 lsp21 -- \
+ lsp-set-addresses lsp21 "f0:00:00:00:02:21 192.168.2.21"
+ovn-nbctl lsp-add ls2 lsp22 -- \
+ lsp-set-addresses lsp22 "f0:00:00:00:02:22 192.168.2.22"
+
+net_add n1
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+sim_add hv2
+as hv2
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.2
+
+as hv1
+ovs-vsctl -- add-port br-int hv1-vif1 -- \
+ set interface hv1-vif1 external-ids:iface-id=lsp11 \
+ 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=lsp21 \
+ options:tx_pcap=hv1/vif2-tx.pcap \
+ options:rxq_pcap=hv1/vif2-rx.pcap \
+ ofport-request=2
+
+ovs-vsctl -- add-port br-int hv1-vif3 -- \
+ set interface hv1-vif3 external-ids:iface-id=lsp22 \
+ options:tx_pcap=hv1/vif3-tx.pcap \
+ options:rxq_pcap=hv1/vif3-rx.pcap \
+ ofport-request=3
+
+# wait for earlier changes to take effect
+check ovn-nbctl --wait=hv sync
+wait_for_ports_up
+
+ovn-sbctl dump-flows > sbflows
+AT_CAPTURE_FILE([sbflows])
+
+AT_CAPTURE_FILE([ofgroups])
+OVS_WAIT_FOR_OUTPUT([as hv1 ovs-ofctl dump-groups br-int > ofgroups
+ grep "selection_method=hash,fields" ofgroups | \
+ grep "nw_proto" | grep "ip_src" | grep "ip_dst" | wc -l], [0], [3
+])
+
+as hv1 ovs-ofctl dump-groups br-int > ofgroups
+AT_CHECK([grep "nw_proto" ofgroups | grep "ip_src" | grep "ip_dst" | grep
"tcp_dst" | wc -l], [0], [1
+])
+AT_CHECK([grep "nw_proto" ofgroups | grep "ip_src" | grep "ip_dst" | grep
"udp_dst" | wc -l], [0], [1
+])
+
+as hv1 ovs-ofctl dump-flows br-int > oflows
+AT_CAPTURE_FILE([oflows])
+
+for i in $(seq 5001 5010); do
+ packet="inport==\"lsp11\" && eth.src==f0:00:00:00:01:11 &&
eth.dst==00:00:00:01:01:01 &&
+ ip4 && ip.ttl==64 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100
&&
+ tcp && tcp.src==$i && tcp.dst==80"
+ OVS_WAIT_UNTIL([as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"])
+
+ for j in 1 2; do
+ # Assume all packets go to lsp2${j}.
+ exp_packet="eth.src==00:00:00:01:02:01 && eth.dst==f0:00:00:00:02:2${j}
&&
+ ip4 && ip.ttl==63 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100
&&
+ tcp && tcp.src==$i && tcp.dst==80"
+ echo $exp_packet | ovstest test-ovn expr-to-packets >>
expected_lsp2${j}
+ done
+done
+
+# All packets should go out of a single port given the hashing is based on
ip_proto,ip_src,ip_dst,dst_port which is fixed
+OVS_WAIT_UNTIL([
+ hv1_rcv_n1=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif2-tx.pcap >
lsp21.packets && cat lsp21.packets | wc -l`
+ hv1_rcv_n2=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif3-tx.pcap >
lsp22.packets && cat lsp22.packets | wc -l`
+ echo $hv1_rcv_n1 $hv1_rcv_n2
+ test $(($hv1_rcv_n1 + $hv1_rcv_n2)) -ge 10])
+
+if test $hv1_rcv_n1 = 0; then
+ AT_CHECK([test $hv1_rcv_n2 -ge 10], [0], [])
+else
+ AT_CHECK([test $hv1_rcv_n1 -ge 10], [0], [])
+fi
+
+# Move all VIFs to hv2 and send the same packets again
+as hv1
+ovs-vsctl del-port hv1-vif1
+ovs-vsctl del-port hv1-vif2
+ovs-vsctl del-port hv1-vif3
+
+wait_column "" Port_Binding chassis logical_port=lsp11
+wait_column "" Port_Binding chassis logical_port=lsp21
+wait_column "" Port_Binding chassis logical_port=lsp22
+
+as hv2
+ovs-vsctl -- add-port br-int hv2-vif1 -- \
+ set interface hv2-vif1 external-ids:iface-id=lsp11 \
+ 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=lsp21 \
+ options:tx_pcap=hv2/vif2-tx.pcap \
+ options:rxq_pcap=hv2/vif2-rx.pcap \
+ ofport-request=2
+
+ovs-vsctl -- add-port br-int hv2-vif3 -- \
+ set interface hv2-vif3 external-ids:iface-id=lsp22 \
+ options:tx_pcap=hv2/vif3-tx.pcap \
+ options:rxq_pcap=hv2/vif3-rx.pcap \
+ ofport-request=3
+
+# wait for earlier changes to take effect
+check ovn-nbctl --wait=hv sync
+wait_for_ports_up
+
+AT_CAPTURE_FILE([ofgroups])
+OVS_WAIT_FOR_OUTPUT([as hv2 ovs-ofctl dump-groups br-int > ofgroups
+ grep "selection_method=hash,fields" ofgroups | \
+ grep "nw_proto" | grep "ip_src" | grep "ip_dst" | wc -l], [0], [3
+])
+
+as hv2 ovs-ofctl dump-groups br-int > ofgroups
+AT_CHECK([grep "nw_proto" ofgroups | grep "ip_src" | grep "ip_dst" | grep
"tcp_dst" | wc -l], [0], [1
+])
+AT_CHECK([grep "nw_proto" ofgroups | grep "ip_src" | grep "ip_dst" | grep
"udp_dst" | wc -l], [0], [1
+])
+
+as hv2 ovs-ofctl dump-flows br-int > oflows
+AT_CAPTURE_FILE([oflows])
+
+for i in $(seq 5001 5010); do
+ packet="inport==\"lsp11\" && eth.src==f0:00:00:00:01:11 &&
eth.dst==00:00:00:01:01:01 &&
+ ip4 && ip.ttl==64 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100
&&
+ tcp && tcp.src==$i && tcp.dst==80"
+ OVS_WAIT_UNTIL([as hv2 ovs-appctl -t ovn-controller inject-pkt "$packet"])
+
+ for j in 1 2; do
+ # Assume all packets go to lsp2${j}.
+ exp_packet="eth.src==00:00:00:01:02:01 && eth.dst==f0:00:00:00:02:2${j}
&&
+ ip4 && ip.ttl==63 && ip4.src==192.168.1.11 && ip4.dst==10.0.0.100
&&
+ tcp && tcp.src==$i && tcp.dst==80"
+ echo $exp_packet | ovstest test-ovn expr-to-packets >>
expected_lsp2${j}
+ done
+done
+
+# All packets should go out of a single port given the hashing is based on
ip_proto,ip_src,ip_dst,dst_port which is fixed
+OVS_WAIT_UNTIL([
+ hv2_rcv_n1=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/vif2-tx.pcap >
lsp21.packets && cat lsp21.packets | wc -l`
+ hv2_rcv_n2=`$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv2/vif3-tx.pcap >
lsp22.packets && cat lsp22.packets | wc -l`
+ echo $hv2_rcv_n1 $hv2_rcv_n2
+ test $(($hv2_rcv_n1 + $hv2_rcv_n2)) -ge 10])
+
+if test $hv2_rcv_n1 = 0; then
+ AT_CHECK([test $hv2_rcv_n2 -ge 10], [0], [])
+else
+ AT_CHECK([test $hv2_rcv_n1 -ge 10], [0], [])
+fi
+
+# All packets should of the same port on both hosts
+if test $hv1_rcv_n1 = 0; then
+ AT_CHECK([test $hv2_rcv_n1 -eq 0], [0], [])
+else
+ AT_CHECK([test $hv2_rcv_n2 -eq 0], [0], [])
+fi
+
+OVN_CLEANUP([hv1], [hv2])
+
+AT_CLEANUP
+])
OVN_FOR_EACH_NORTHD([
AT_SETUP([route tables -- <main> route table routes])
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 679d3f2d9..70e8fb239 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -4720,11 +4720,18 @@ nbctl_lr_route_add(struct ctl_context *ctx)
nbrec_logical_router_static_route_set_route_table(route, route_table);
}
- if (ecmp_symmetric_reply) {
- const struct smap options = SMAP_CONST1(&options,
- "ecmp_symmetric_reply",
- "true");
+ const char *ecmp_selection_fields = shash_find_data(&ctx->options,
+ "--ecmp-selection-fields");
+ if (ecmp_symmetric_reply || ecmp_selection_fields) {
+ struct smap options = SMAP_INITIALIZER(&options);
+ if (ecmp_symmetric_reply) {
+ smap_add(&options, "ecmp_symmetric_reply", "true");
+ }
+ if (ecmp_selection_fields) {
+ smap_add(&options, "ecmp_selection_fields", ecmp_selection_fields);
+ }
nbrec_logical_router_static_route_set_options(route, &options);
+ smap_destroy(&options);
}
nbrec_logical_router_update_static_routes_addvalue(lr, route);
@@ -8057,7 +8064,7 @@ static const struct ctl_command_syntax nbctl_commands[] =
{
{ "lr-route-add", 3, 4, "ROUTER PREFIX NEXTHOP [PORT]",
nbctl_pre_lr_route_add, nbctl_lr_route_add, NULL,
"--may-exist,--ecmp,--ecmp-symmetric-reply,--policy=,"
- "--route-table=,--bfd?", RW },
+ "--route-table=,--bfd?,--ecmp-selection-fields=", RW },
{ "lr-route-del", 1, 4, "ROUTER [PREFIX [NEXTHOP [PORT]]]",
nbctl_pre_lr_route_del, nbctl_lr_route_del, NULL,
"--if-exists,--policy=,--route-table=", RW },