This patch implements one approach to using ovn-controller to implement a software l2 gateway between logical and physical networks.
A new logical port type called "gateway" is introduced here. It is very close to how localnet ports work, with the following exception: - A localnet port makes OVN use the physical network as the transport between hypervisors instead of tunnels. A gateway port still uses tunnels between all hypervisors, and packets only go to/from the specified physical network as needed via the chassis the gateway port is bound to. - A gateway port also gets bound to a chassis while a localnet port does not. This binding is not done by ovn-controller. It is left as an administrative function. In the case of OpenStack, the Neutron plugin will do this. Signed-off-by: Russell Bryant <russ...@ovn.org> --- v1->v2: - Rebase and resolve conflicts with master. - Fix doc typo when descriping gateway port type. - Ensure chassis_id is non-NULL before calling patch_run(). ovn/controller/binding.c | 9 ++ ovn/controller/ovn-controller.8.xml | 15 ++-- ovn/controller/ovn-controller.c | 5 +- ovn/controller/patch.c | 25 ++++-- ovn/controller/patch.h | 3 +- ovn/controller/physical.c | 10 ++- ovn/ovn-nb.xml | 19 +++++ ovn/ovn-sb.xml | 41 ++++++++- tests/ovn.at | 164 ++++++++++++++++++++++++++++++++++++ 9 files changed, 270 insertions(+), 21 deletions(-) diff --git a/ovn/controller/binding.c b/ovn/controller/binding.c index d3ca9c9..de18017 100644 --- a/ovn/controller/binding.c +++ b/ovn/controller/binding.c @@ -205,6 +205,15 @@ binding_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int, } sbrec_port_binding_set_chassis(binding_rec, chassis_rec); } + } else if (!strcmp(binding_rec->type, "gateway") + && binding_rec->chassis == chassis_rec) { + /* A locally bound gateway port. + * + * ovn-controller does not bind gateway ports itself. + * Choosing a chassis * for a gateway port is left + * up to an entity external to OVN. */ + sset_add(&all_lports, binding_rec->logical_port); + add_local_datapath(local_datapaths, binding_rec); } else if (binding_rec->chassis == chassis_rec) { if (ctx->ovnsb_idl_txn) { VLOG_INFO("Releasing lport %s from this chassis.", diff --git a/ovn/controller/ovn-controller.8.xml b/ovn/controller/ovn-controller.8.xml index 1ee3a6e..4c11c4c 100644 --- a/ovn/controller/ovn-controller.8.xml +++ b/ovn/controller/ovn-controller.8.xml @@ -183,18 +183,19 @@ <p> The presence of this key identifies a patch port as one created by <code>ovn-controller</code> to connect the integration bridge and - another bridge to implement a <code>localnet</code> logical port. - Its value is the name of the logical port with type=localnet that - the port implements. + another bridge to implement a <code>localnet</code> or + <code>gateway</code> logical port. Its value is the name of the + logical port with <code>type</code> set to <code>localnet</code> + or <code>gateway</code> that the port implements. See <code>external_ids:ovn-bridge-mappings</code>, above, for more information. </p> <p> - Each <code>localnet</code> logical port is implemented as a pair of - patch ports, one in the integration bridge, one in a different - bridge, with the same <code>external-ids:ovn-localnet-port</code> - value. + Each <code>localnet</code> and <code>gateway</code> logical port + is implemented as a pair of patch ports, one in the integration + bridge, one in a different bridge, with the same + <code>external-ids:ovn-localnet-port</code> value. </p> </dd> diff --git a/ovn/controller/ovn-controller.c b/ovn/controller/ovn-controller.c index 6027011..9bcda0d 100644 --- a/ovn/controller/ovn-controller.c +++ b/ovn/controller/ovn-controller.c @@ -326,7 +326,10 @@ main(int argc, char *argv[]) } if (br_int) { - patch_run(&ctx, br_int, &local_datapaths, &patched_datapaths); + if (chassis_id) { + patch_run(&ctx, br_int, &local_datapaths, &patched_datapaths, + chassis_id); + } struct lport_index lports; struct mcgroup_index mcgroups; diff --git a/ovn/controller/patch.c b/ovn/controller/patch.c index 943ac99..6c666b8 100644 --- a/ovn/controller/patch.c +++ b/ovn/controller/patch.c @@ -134,7 +134,8 @@ static void add_bridge_mappings(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int, struct shash *existing_ports, - struct hmap *local_datapaths) + struct hmap *local_datapaths, + const char *chassis_id) { /* Get ovn-bridge-mappings. */ const char *mappings_cfg = ""; @@ -196,23 +197,30 @@ add_bridge_mappings(struct controller_ctx *ctx, continue; } ld->localnet_port = binding; + } else if (!strcmp(binding->type, "gateway")) { + if (!binding->chassis || strcmp(chassis_id, binding->chassis->name)) { + /* This gateway port is not bound to this chassis, so we should + * not create any patch ports for it. */ + continue; + } } else { - /* Not a binding for a localnet port. */ + /* not a localnet or gateway port. */ continue; } const char *network = smap_get(&binding->options, "network_name"); if (!network) { static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1); - VLOG_ERR_RL(&rl, "localnet port '%s' has no network name.", - binding->logical_port); + VLOG_ERR_RL(&rl, "%s port '%s' has no network name.", + binding->type, binding->logical_port); continue; } struct ovsrec_bridge *br_ln = shash_find_data(&bridge_mappings, network); if (!br_ln) { static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1); - VLOG_ERR_RL(&rl, "bridge not found for localnet port '%s' " - "with network name '%s'", binding->logical_port, network); + VLOG_ERR_RL(&rl, "bridge not found for %s port '%s' " + "with network name '%s'", + binding->type, binding->logical_port, network); continue; } @@ -294,7 +302,8 @@ add_logical_patch_ports(struct controller_ctx *ctx, void patch_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int, - struct hmap *local_datapaths, struct hmap *patched_datapaths) + struct hmap *local_datapaths, struct hmap *patched_datapaths, + const char *chassis_id) { if (!ctx->ovs_idl_txn) { return; @@ -313,7 +322,7 @@ patch_run(struct controller_ctx *ctx, const struct ovsrec_bridge *br_int, /* Create in the database any patch ports that should exist. Remove from * 'existing_ports' any patch ports that do exist in the database and * should be there. */ - add_bridge_mappings(ctx, br_int, &existing_ports, local_datapaths); + add_bridge_mappings(ctx, br_int, &existing_ports, local_datapaths, chassis_id); add_logical_patch_ports(ctx, br_int, &existing_ports, patched_datapaths); /* Now 'existing_ports' only still contains patch ports that exist in the diff --git a/ovn/controller/patch.h b/ovn/controller/patch.h index d5d842e..5ff0a2b 100644 --- a/ovn/controller/patch.h +++ b/ovn/controller/patch.h @@ -27,6 +27,7 @@ struct hmap; struct ovsrec_bridge; void patch_run(struct controller_ctx *, const struct ovsrec_bridge *br_int, - struct hmap *local_datapaths, struct hmap *patched_datapaths); + struct hmap *local_datapaths, struct hmap *patched_datapaths, + const char *chassis_id); #endif /* ovn/patch.h */ diff --git a/ovn/controller/physical.c b/ovn/controller/physical.c index 795db31..9eeb790 100644 --- a/ovn/controller/physical.c +++ b/ovn/controller/physical.c @@ -299,7 +299,9 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve, } else { ofport = u16_to_ofp(simap_get(&localvif_to_ofport, binding->logical_port)); - if (!strcmp(binding->type, "localnet") && ofport && binding->tag) { + if ((!strcmp(binding->type, "localnet") + || !strcmp(binding->type, "gateway")) + && ofport && binding->tag) { tag = *binding->tag; } } @@ -358,7 +360,8 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve, /* Match a VLAN tag and strip it, including stripping priority tags * (e.g. VLAN ID 0). In the latter case we'll add a second flow * for frames that lack any 802.1Q header later. */ - if (tag || !strcmp(binding->type, "localnet")) { + if (tag || !strcmp(binding->type, "localnet") + || !strcmp(binding->type, "gateway")) { match_set_dl_vlan(&match, htons(tag)); ofpact_put_STRIP_VLAN(&ofpacts); } @@ -382,7 +385,8 @@ physical_run(struct controller_ctx *ctx, enum mf_field_id mff_ovn_geneve, ofctrl_add_flow(flow_table, OFTABLE_PHY_TO_LOG, tag ? 150 : 100, &match, &ofpacts); - if (!tag && !strcmp(binding->type, "localnet")) { + if (!tag && (!strcmp(binding->type, "localnet") + || !strcmp(binding->type, "gateway"))) { /* Add a second flow for frames that lack any 802.1Q * header. For these, drop the OFPACT_STRIP_VLAN * action. */ diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml index e65bc3a..2318741 100644 --- a/ovn/ovn-nb.xml +++ b/ovn/ovn-nb.xml @@ -134,6 +134,11 @@ to model direct connectivity to an existing network. </dd> + <dt><code>gateway</code></dt> + <dd> + A connection to a physical network. + </dd> + <dt><code>vtep</code></dt> <dd> A port to a logical switch on a VTEP gateway. @@ -182,6 +187,20 @@ </column> </group> + <group title="Options for gateway ports"> + <p> + These options apply when <ref column="type"/> is + <code>gateway</code>. + </p> + + <column name="options" key="network_name"> + Required. The name of the network to which the <code>gateway</code> + port is connected. The gateway, via <code>ovn-controller</code>, + uses its local configuration to determine exactly how to connect to + this network. + </column> + </group> + <group title="Options for vtep ports"> <p> These options apply when <ref column="type"/> is <code>vtep</code>. diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml index efd2f9a..31eb113 100644 --- a/ovn/ovn-sb.xml +++ b/ovn/ovn-sb.xml @@ -1306,6 +1306,14 @@ tcp.flags = RST; to model direct connectivity to an existing network. </dd> + <dt><code>gateway</code></dt> + <dd> + A connection to a physical network. The chassis this + <ref table="Port_Binding"/> is bound to will serve as + an L2 gateway to the network named by + <ref column="options" table="Port_Binding"/>:<code>network_name</code>. + </dd> + <dt><code>vtep</code></dt> <dd> A port to a logical switch on a VTEP gateway chassis. In order to @@ -1368,6 +1376,36 @@ tcp.flags = RST; </column> </group> + <group title="Gateway Options"> + <p> + These options apply to logical ports with <ref column="type"/> of + <code>gateway</code>. + </p> + + <column name="options" key="network_name"> + Required. <code>ovn-controller</code> uses the configuration entry + <code>ovn-bridge-mappings</code> to determine how to connect to this + network. <code>ovn-bridge-mappings</code> is a list of network names + mapped to a local OVS bridge that provides access to that network. An + example of configuring <code>ovn-bridge-mappings</code> would be: + + <pre>$ ovs-vsctl set open . external-ids:ovn-bridge-mappings=physnet1:br-eth0,physnet2:br-eth1</pre> + + <p> + When a logical switch has a <code>gateway</code> port attached, + the chassis that the <code>gateway</code> port is bound to + must have a bridge mapping configured to reach the network + identified by <code>network_name</code>. + </p> + </column> + + <column name="tag"> + If set, indicates that the gateway is connected to a specific + VLAN on the physical network. The VLAN ID is used to match + incoming traffic and is also added to outgoing traffic. + </column> + </group> + <group title="VTEP Options"> <p> These options apply to logical ports with <ref column="type"/> of @@ -1425,7 +1463,8 @@ tcp.flags = RST; <p> This column is used for a different purpose when <ref column="type"/> - is <code>localnet</code> (see <code>Localnet Options</code>, above). + is <code>localnet</code> (see <code>Localnet Options</code>, above) + or <code>gateway</code> (see <code>Gateway Options</code>, above). </p> </column> </group> diff --git a/tests/ovn.at b/tests/ovn.at index 22121e1..cd0a0cb 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -1225,6 +1225,170 @@ for sim in hv1 hv2 hv3 vtep main; do done AT_CLEANUP +# Similar test to "hardware GW" +AT_SETUP([ovn -- 3 HVs, 1 VIFs/HV, 1 software GW, 1 LS]) +AT_SKIP_IF([test $HAVE_PYTHON = no]) +ovn_start + +# Configure the Northbound database +ovn-nbctl lswitch-add lsw0 + +ovn-nbctl lport-add lsw0 lp1 +ovn-nbctl lport-set-addresses lp1 f0:00:00:00:00:01 + +ovn-nbctl lport-add lsw0 lp2 +ovn-nbctl lport-set-addresses lp2 f0:00:00:00:00:02 + +ovn-nbctl lport-add lsw0 lp-gw +ovn-nbctl lport-set-type lp-gw gateway +ovn-nbctl lport-set-options lp-gw network_name=physnet1 +ovn-nbctl lport-set-addresses lp-gw unknown + +net_add n1 # Network to connect hv1, hv2, and gw +net_add n2 # Network to connect gw and hv3 + +# Create hypervisor hv1 connected to 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 vif1 -- set Interface vif1 external-ids:iface-id=lp1 options:tx_pcap=hv1/vif1-tx.pcap options:rxq_pcap=hv1/vif1-rx.pcap ofport-request=1 + +# Create hypervisor hv2 connected to n1 +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 vif2 -- set Interface vif2 external-ids:iface-id=lp2 options:tx_pcap=hv2/vif2-tx.pcap options:rxq_pcap=hv2/vif2-rx.pcap ofport-request=1 + +# Create hypervisor hv_gw connected to n1 and n2 +# connect br-phys bridge to n1; connect hv-gw bridge to n2 +sim_add hv_gw +as hv_gw +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.3 +ovs-vsctl add-br br-phys2 +net_attach n2 br-phys2 +ovs-vsctl set open . external_ids:ovn-bridge-mappings="physnet1:br-phys2" + +# Bind our gateway port to the hv_gw chassis +ovn-sbctl lport-bind lp-gw hv_gw + +# Add hv3 on the other side of the GW +sim_add hv3 +as hv3 +ovs-vsctl add-br br-phys +net_attach n2 br-phys +ovs-vsctl add-port br-phys vif3 -- set Interface vif3 options:tx_pcap=hv3/vif3-tx.pcap options:rxq_pcap=hv3/vif3-rx.pcap ofport-request=1 + + +# Pre-populate the hypervisors' ARP tables so that we don't lose any +# packets for ARP resolution (native tunneling doesn't queue packets +# for ARP resolution). +ovn_populate_arp + +# Allow some time for ovn-northd and ovn-controller to catch up. +# XXX This should be more systematic. +sleep 1 + +# test_packet INPORT DST SRC ETHTYPE OUTPORT... +# +# This shell function causes a packet to be received on INPORT. The packet's +# content has Ethernet destination DST and source SRC (each exactly 12 hex +# digits) and Ethernet type ETHTYPE (4 hex digits). The OUTPORTs (zero or +# more) list the VIFs on which the packet should be received. INPORT and the +# OUTPORTs are specified as lport numbers, e.g. 1 for vif1. +trim_zeros() { + sed 's/\(00\)\{1,\}$//' +} +for i in 1 2 3; do + : > $i.expected +done +test_packet() { + local inport=$1 packet=$2$3$4; shift; shift; shift; shift + #hv=hv`echo $inport | sed 's/^\(.\).*/\1/'` + hv=hv$inport + vif=vif$inport + as $hv ovs-appctl netdev-dummy/receive $vif $packet + for outport; do + echo $packet | trim_zeros >> $outport.expected + done +} + +# Send packets between all pairs of source and destination ports: +# +# 1. Unicast packets are delivered to exactly one lport (except that packets +# destined to their input ports are dropped). +# +# 2. Broadcast and multicast are delivered to all lports except the input port. +# +# 3. The lswitch delivers packets with an unknown destination to lports with +# "unknown" among their MAC addresses (and port security disabled). +for s in 1 2 3 ; do + bcast= + unknown= + for d in 1 2 3 ; do + if test $d != $s; then unicast=$d; else unicast=; fi + test_packet $s f0000000000$d f0000000000$s 00$s$d $unicast #1 + + # The vtep (vif3) is the only one configured for "unknown" + if test $d != $s && test $d = 3; then + unknown="$unknown $d" + fi + bcast="$bcast $unicast" + done + + test_packet $s ffffffffffff f0000000000$s 0${s}ff $bcast #2 + test_packet $s 010000000000 f0000000000$s 0${s}ff $bcast #3 + test_packet $s f0000000ffff f0000000000$s 0${s}66 $unknown #4 +done + +# Allow some time for packet forwarding. +# XXX This can be improved. +sleep 3 + +echo "------ ovn-nbctl show ------" +ovn-nbctl show +echo "------ ovn-sbctl show ------" +ovn-sbctl show + +echo "------ hv1 ------" +as hv1 ovs-vsctl show +echo "------ hv1 br-int ------" +as hv1 ovs-ofctl -O OpenFlow13 dump-flows br-int +echo "------ hv1 br-phys ------" +as hv1 ovs-ofctl -O OpenFlow13 dump-flows br-phys + +echo "------ hv2 ------" +as hv2 ovs-vsctl show +echo "------ hv2 br-int ------" +as hv2 ovs-ofctl -O OpenFlow13 dump-flows br-int +echo "------ hv2 br-phys ------" +as hv2 ovs-ofctl -O OpenFlow13 dump-flows br-phys + +echo "------ hv_gw ------" +as hv_gw ovs-vsctl show +echo "------ hv_gw br-phys ------" +as hv_gw ovs-ofctl -O OpenFlow13 dump-flows br-phys +echo "------ hv_gw br-phys2 ------" +as hv_gw ovs-ofctl -O OpenFlow13 dump-flows br-phys2 + +echo "------ hv3 ------" +as hv3 ovs-vsctl show +echo "------ hv3 br-phys ------" +as hv3 ovs-ofctl -O OpenFlow13 dump-flows br-phys + +# Now check the packets actually received against the ones expected. +for i in 1 2 3; do + file=hv$i/vif$i-tx.pcap + echo $file + $PYTHON "$top_srcdir/utilities/ovs-pcap.in" $file | trim_zeros > $i.packets + sort $i.expected > expout + AT_CHECK([sort $i.packets], [0], [expout]) + echo +done +AT_CLEANUP + # 3 hypervisors, 3 logical switches with 3 logical ports each, 1 logical router AT_SETUP([ovn -- 3 HVs, 3 LS, 3 lports/LS, 1 LR]) AT_SKIP_IF([test $HAVE_PYTHON = no]) -- 2.5.5 _______________________________________________ dev mailing list dev@openvswitch.org http://openvswitch.org/mailman/listinfo/dev