From: Ben Pfaff Date: Tue, 9 Aug 2011 16:24:18 +0000 (-0700) Subject: New action NXAST_RESUBMIT_TABLE. X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=299016266ed1;p=openvswitch New action NXAST_RESUBMIT_TABLE. This makes multiple table support in ofproto-dpif useful, by allowing resubmits into tables other than 0. --- diff --git a/NEWS b/NEWS index b31b127a..a55af137 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,12 @@ Post-v1.2.0 ------------------------ - ovs-appctl: - New "version" command to determine version of running daemon + - ovs-vswitchd: + - The software switch now supports 255 OpenFlow tables, instead + of just one. By default, only table 0 is consulted, but the + new NXAST_RESUBMIT_TABLE action can look up in additional + tables. Tables 128 and above are reserved for use by the + switch itself; please use only tables 0 through 127. v1.2.0 - 03 Aug 2011 ------------------------ diff --git a/include/openflow/nicira-ext.h b/include/openflow/nicira-ext.h index 5cf02e7c..776ec391 100644 --- a/include/openflow/nicira-ext.h +++ b/include/openflow/nicira-ext.h @@ -279,7 +279,8 @@ enum nx_action_subtype { NXAST_MULTIPATH, /* struct nx_action_multipath */ NXAST_AUTOPATH, /* struct nx_action_autopath */ NXAST_BUNDLE, /* struct nx_action_bundle */ - NXAST_BUNDLE_LOAD /* struct nx_action_bundle */ + NXAST_BUNDLE_LOAD, /* struct nx_action_bundle */ + NXAST_RESUBMIT_TABLE /* struct nx_action_resubmit */ }; /* Header for Nicira-defined actions. */ @@ -292,31 +293,51 @@ struct nx_action_header { }; OFP_ASSERT(sizeof(struct nx_action_header) == 16); -/* Action structure for NXAST_RESUBMIT. +/* Action structures for NXAST_RESUBMIT and NXAST_RESUBMIT_TABLE. * - * NXAST_RESUBMIT searches the flow table again, using a flow that is slightly - * modified from the original lookup: + * These actions search one of the switch's flow tables: * - * - The 'in_port' member of struct nx_action_resubmit is used as the flow's - * in_port. + * - For NXAST_RESUBMIT_TABLE only, if the 'table' member is not 255, then + * it specifies the table to search. * - * - If NXAST_RESUBMIT is preceded by actions that affect the flow - * (e.g. OFPAT_SET_VLAN_VID), then the flow is updated with the new - * values. + * - Otherwise (for NXAST_RESUBMIT_TABLE with a 'table' of 255, or for + * NXAST_RESUBMIT regardless of 'table'), it searches the current flow + * table, that is, the OpenFlow flow table that contains the flow from + * which this action was obtained. If this action did not come from a + * flow table (e.g. it came from an OFPT_PACKET_OUT message), then table 0 + * is the current table. + * + * The flow table lookup uses a flow that may be slightly modified from the + * original lookup: + * + * - For NXAST_RESUBMIT, the 'in_port' member of struct nx_action_resubmit + * is used as the flow's in_port. + * + * - For NXAST_RESUBMIT_TABLE, if the 'in_port' member is not OFPP_IN_PORT, + * then its value is used as the flow's in_port. Otherwise, the original + * in_port is used. + * + * - If actions that modify the flow (e.g. OFPAT_SET_VLAN_VID) precede the + * resubmit action, then the flow is updated with the new values. * * Following the lookup, the original in_port is restored. * * If the modified flow matched in the flow table, then the corresponding - * actions are executed. Afterward, actions following NXAST_RESUBMIT in the + * actions are executed. Afterward, actions following the resubmit in the * original set of actions, if any, are executed; any changes made to the * packet (e.g. changes to VLAN) by secondary actions persist when those * actions are executed, although the original in_port is restored. * - * NXAST_RESUBMIT may be used any number of times within a set of actions. + * Resubmit actions may be used any number of times within a set of actions. + * + * Resubmit actions may nest to an implementation-defined depth. Beyond this + * implementation-defined depth, further resubmit actions are simply ignored. + * + * NXAST_RESUBMIT ignores 'table' and 'pad'. NXAST_RESUBMIT_TABLE requires + * 'pad' to be all-bits-zero. * - * NXAST_RESUBMIT may nest to an implementation-defined depth. Beyond this - * implementation-defined depth, further NXAST_RESUBMIT actions are simply - * ignored. (Open vSwitch 1.0.1 and earlier did not support recursion.) + * Open vSwitch 1.0.1 and earlier did not support recursion. Open vSwitch + * before 1.2.90 did not support NXAST_RESUBMIT_TABLE. */ struct nx_action_resubmit { ovs_be16 type; /* OFPAT_VENDOR. */ @@ -324,7 +345,8 @@ struct nx_action_resubmit { ovs_be32 vendor; /* NX_VENDOR_ID. */ ovs_be16 subtype; /* NXAST_RESUBMIT. */ ovs_be16 in_port; /* New in_port for checking flow table. */ - uint8_t pad[4]; + uint8_t table; /* NXAST_RESUBMIT_TABLE: table to use. */ + uint8_t pad[3]; }; OFP_ASSERT(sizeof(struct nx_action_resubmit) == 16); diff --git a/lib/ofp-parse.c b/lib/ofp-parse.c index 58b0da1e..c171293c 100644 --- a/lib/ofp-parse.c +++ b/lib/ofp-parse.c @@ -285,7 +285,6 @@ put_dl_addr_action(struct ofpbuf *b, uint16_t type, const char *addr) str_to_mac(addr, oada->dl_addr); } - static bool parse_port_name(const char *name, uint16_t *port) { @@ -317,6 +316,40 @@ parse_port_name(const char *name, uint16_t *port) return false; } +static void +parse_resubmit(struct nx_action_resubmit *nar, char *arg) +{ + char *in_port_s, *table_s; + uint16_t in_port; + uint8_t table; + + in_port_s = strsep(&arg, ","); + if (in_port_s && in_port_s[0]) { + if (!parse_port_name(in_port_s, &in_port)) { + in_port = str_to_u32(in_port_s); + } + } else { + in_port = OFPP_IN_PORT; + } + + table_s = strsep(&arg, ","); + table = table_s && table_s[0] ? str_to_u32(table_s) : 255; + + if (in_port == OFPP_IN_PORT && table == 255) { + ovs_fatal(0, "at least one \"in_port\" or \"table\" must be specified " + " on resubmit"); + } + + nar->vendor = htonl(NX_VENDOR_ID); + nar->in_port = htons(in_port); + if (in_port != OFPP_IN_PORT && table == 255) { + nar->subtype = htons(NXAST_RESUBMIT); + } else { + nar->subtype = htons(NXAST_RESUBMIT_TABLE); + nar->table = table; + } +} + static void str_to_action(char *str, struct ofpbuf *b) { @@ -421,9 +454,7 @@ str_to_action(char *str, struct ofpbuf *b) } else if (!strcasecmp(act, "resubmit")) { struct nx_action_resubmit *nar; nar = put_action(b, sizeof *nar, OFPAT_VENDOR); - nar->vendor = htonl(NX_VENDOR_ID); - nar->subtype = htons(NXAST_RESUBMIT); - nar->in_port = htons(str_to_u32(arg)); + parse_resubmit(nar, arg); } else if (!strcasecmp(act, "set_tunnel") || !strcasecmp(act, "set_tunnel64")) { uint64_t tun_id = str_to_u64(arg); diff --git a/lib/ofp-print.c b/lib/ofp-print.c index 0265f301..9311c14f 100644 --- a/lib/ofp-print.c +++ b/lib/ofp-print.c @@ -295,6 +295,19 @@ ofp_print_action(struct ds *s, const union ofp_action *a, ofp_print_port_name(s, ntohs(nar->in_port)); break; + case OFPUTIL_NXAST_RESUBMIT_TABLE: + nar = (struct nx_action_resubmit *)a; + ds_put_format(s, "resubmit("); + if (nar->in_port != htons(OFPP_IN_PORT)) { + ofp_print_port_name(s, ntohs(nar->in_port)); + } + ds_put_char(s, ','); + if (nar->table != 255) { + ds_put_format(s, "%"PRIu8, nar->table); + } + ds_put_char(s, ')'); + break; + case OFPUTIL_NXAST_SET_TUNNEL: nast = (struct nx_action_set_tunnel *)a; ds_put_format(s, "set_tunnel:%#"PRIx32, ntohl(nast->tun_id)); diff --git a/lib/ofp-util.c b/lib/ofp-util.c index d9ebcda7..579be73f 100644 --- a/lib/ofp-util.c +++ b/lib/ofp-util.c @@ -1966,6 +1966,15 @@ ofputil_check_output_port(uint16_t port, int max_ports) } } +static int +check_resubmit_table(const struct nx_action_resubmit *nar) +{ + if (nar->pad[0] || nar->pad[1] || nar->pad[2]) { + return ofp_mkerr(OFPET_BAD_ACTION, OFPBAC_BAD_ARGUMENT); + } + return 0; +} + int validate_actions(const union ofp_action *actions, size_t n_actions, const struct flow *flow, int max_ports) @@ -2044,6 +2053,11 @@ validate_actions(const union ofp_action *actions, size_t n_actions, max_ports, flow); break; + case OFPUTIL_NXAST_RESUBMIT_TABLE: + error = check_resubmit_table( + (const struct nx_action_resubmit *) a); + break; + case OFPUTIL_OFPAT_STRIP_VLAN: case OFPUTIL_OFPAT_SET_NW_SRC: case OFPUTIL_OFPAT_SET_NW_DST: @@ -2151,6 +2165,7 @@ ofputil_decode_nxast_action(const union ofp_action *a) NXAST_ACTION(NXAST_AUTOPATH, struct nx_action_autopath, false); NXAST_ACTION(NXAST_BUNDLE, struct nx_action_bundle, true); NXAST_ACTION(NXAST_BUNDLE_LOAD, struct nx_action_bundle, true); + NXAST_ACTION(NXAST_RESUBMIT_TABLE, struct nx_action_resubmit, false); #undef NXAST_ACTION case NXAST_SNAT__OBSOLETE: diff --git a/lib/ofp-util.h b/lib/ofp-util.h index dfd47be6..a9601aa1 100644 --- a/lib/ofp-util.h +++ b/lib/ofp-util.h @@ -304,6 +304,7 @@ enum ofputil_action_code { OFPUTIL_NXAST_AUTOPATH, OFPUTIL_NXAST_BUNDLE, OFPUTIL_NXAST_BUNDLE_LOAD, + OFPUTIL_NXAST_RESUBMIT_TABLE }; int ofputil_decode_action(const union ofp_action *); diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c index a865d21c..8b65becf 100644 --- a/ofproto/ofproto-dpif.c +++ b/ofproto/ofproto-dpif.c @@ -60,7 +60,7 @@ COVERAGE_DEFINE(facet_invalidated); COVERAGE_DEFINE(facet_revalidate); COVERAGE_DEFINE(facet_unexpected); -/* Maximum depth of flow table recursion (due to NXAST_RESUBMIT actions) in a +/* Maximum depth of flow table recursion (due to resubmit actions) in a * flow translation. */ #define MAX_RESUBMIT_RECURSION 16 @@ -96,8 +96,8 @@ static struct rule_dpif *rule_dpif_cast(const struct rule *rule) return rule ? CONTAINER_OF(rule, struct rule_dpif, up) : NULL; } -static struct rule_dpif *rule_dpif_lookup(struct ofproto_dpif *ofproto, - const struct flow *flow); +static struct rule_dpif *rule_dpif_lookup(struct ofproto_dpif *, + const struct flow *, uint8_t table); #define MAX_MIRRORS 32 typedef uint32_t mirror_mask_t; @@ -188,6 +188,7 @@ struct action_xlate_ctx { uint32_t priority; /* Current flow priority. 0 if none. */ struct flow base_flow; /* Flow at the last commit. */ uint32_t base_priority; /* Priority at the last commit. */ + uint8_t table_id; /* OpenFlow table ID where flow was found. */ }; static void action_xlate_ctx_init(struct action_xlate_ctx *, @@ -1654,7 +1655,7 @@ handle_miss_upcall(struct ofproto_dpif *ofproto, struct dpif_upcall *upcall) facet = facet_lookup_valid(ofproto, &flow); if (!facet) { - struct rule_dpif *rule = rule_dpif_lookup(ofproto, &flow); + struct rule_dpif *rule = rule_dpif_lookup(ofproto, &flow, 0); if (!rule) { /* Don't send a packet-in if OFPPC_NO_PACKET_IN asserted. */ struct ofport_dpif *port = get_ofp_port(ofproto, flow.in_port); @@ -2434,7 +2435,7 @@ facet_revalidate(struct ofproto_dpif *ofproto, struct facet *facet) COVERAGE_INC(facet_revalidate); /* Determine the new rule. */ - new_rule = rule_dpif_lookup(ofproto, &facet->flow); + new_rule = rule_dpif_lookup(ofproto, &facet->flow, 0); if (!new_rule) { /* No new rule, so delete the facet. */ facet_remove(ofproto, facet); @@ -2592,10 +2593,11 @@ flow_push_stats(const struct rule_dpif *rule, /* Rules. */ static struct rule_dpif * -rule_dpif_lookup(struct ofproto_dpif *ofproto, const struct flow *flow) +rule_dpif_lookup(struct ofproto_dpif *ofproto, const struct flow *flow, + uint8_t table_id) { return rule_dpif_cast(rule_from_cls_rule( - classifier_lookup(&ofproto->up.tables[0], + classifier_lookup(&ofproto->up.tables[table_id], flow))); } @@ -2717,7 +2719,7 @@ rule_execute(struct rule *rule_, struct flow *flow, struct ofpbuf *packet) /* Otherwise, if 'rule' is in fact the correct rule for 'packet', then * create a new facet for it and use that. */ - if (rule_dpif_lookup(ofproto, flow) == rule) { + if (rule_dpif_lookup(ofproto, flow, 0) == rule) { facet = facet_create(rule, flow, packet); facet_execute(ofproto, facet, packet); facet_install(ofproto, facet, true); @@ -2889,18 +2891,23 @@ add_output_action(struct action_xlate_ctx *ctx, uint16_t ofp_port) } static void -xlate_table_action(struct action_xlate_ctx *ctx, uint16_t in_port) +xlate_table_action(struct action_xlate_ctx *ctx, + uint16_t in_port, uint8_t table_id) { if (ctx->recurse < MAX_RESUBMIT_RECURSION) { struct rule_dpif *rule; uint16_t old_in_port; + uint8_t old_table_id; + + old_table_id = ctx->table_id; + ctx->table_id = table_id; /* Look up a flow with 'in_port' as the input port. Then restore the * original input port (otherwise OFPP_NORMAL and OFPP_IN_PORT will * have surprising behavior). */ old_in_port = ctx->flow.in_port; ctx->flow.in_port = in_port; - rule = rule_dpif_lookup(ctx->ofproto, &ctx->flow); + rule = rule_dpif_lookup(ctx->ofproto, &ctx->flow, table_id); ctx->flow.in_port = old_in_port; if (ctx->resubmit_hook) { @@ -2912,14 +2919,31 @@ xlate_table_action(struct action_xlate_ctx *ctx, uint16_t in_port) do_xlate_actions(rule->up.actions, rule->up.n_actions, ctx); ctx->recurse--; } + + ctx->table_id = old_table_id; } else { static struct vlog_rate_limit recurse_rl = VLOG_RATE_LIMIT_INIT(1, 1); - VLOG_ERR_RL(&recurse_rl, "NXAST_RESUBMIT recursed over %d times", + VLOG_ERR_RL(&recurse_rl, "resubmit actions recursed over %d times", MAX_RESUBMIT_RECURSION); } } +static void +xlate_resubmit_table(struct action_xlate_ctx *ctx, + const struct nx_action_resubmit *nar) +{ + uint16_t in_port; + uint8_t table_id; + + in_port = (nar->in_port == htons(OFPP_IN_PORT) + ? ctx->flow.in_port + : ntohs(nar->in_port)); + table_id = nar->table == 255 ? ctx->table_id : nar->table; + + xlate_table_action(ctx, in_port, table_id); +} + static void flood_packets(struct action_xlate_ctx *ctx, ovs_be32 mask) { @@ -2950,7 +2974,7 @@ xlate_output_action__(struct action_xlate_ctx *ctx, add_output_action(ctx, ctx->flow.in_port); break; case OFPP_TABLE: - xlate_table_action(ctx, ctx->flow.in_port); + xlate_table_action(ctx, ctx->flow.in_port, ctx->table_id); break; case OFPP_NORMAL: xlate_normal(ctx); @@ -3182,7 +3206,11 @@ do_xlate_actions(const union ofp_action *in, size_t n_in, case OFPUTIL_NXAST_RESUBMIT: nar = (const struct nx_action_resubmit *) ia; - xlate_table_action(ctx, ntohs(nar->in_port)); + xlate_table_action(ctx, ntohs(nar->in_port), ctx->table_id); + break; + + case OFPUTIL_NXAST_RESUBMIT_TABLE: + xlate_resubmit_table(ctx, (const struct nx_action_resubmit *) ia); break; case OFPUTIL_NXAST_SET_TUNNEL: @@ -3272,6 +3300,7 @@ xlate_actions(struct action_xlate_ctx *ctx, ctx->priority = 0; ctx->base_priority = 0; ctx->base_flow = ctx->flow; + ctx->table_id = 0; if (process_special(ctx->ofproto, &ctx->flow, ctx->packet)) { ctx->may_set_up_flow = false; @@ -3925,7 +3954,8 @@ struct ofproto_trace { }; static void -trace_format_rule(struct ds *result, int level, const struct rule_dpif *rule) +trace_format_rule(struct ds *result, uint8_t table_id, int level, + const struct rule_dpif *rule) { ds_put_char_multiple(result, '\t', level); if (!rule) { @@ -3933,8 +3963,8 @@ trace_format_rule(struct ds *result, int level, const struct rule_dpif *rule) return; } - ds_put_format(result, "Rule: cookie=%#"PRIx64" ", - ntohll(rule->up.flow_cookie)); + ds_put_format(result, "Rule: table=%"PRIu8" cookie=%#"PRIx64" ", + table_id, ntohll(rule->up.flow_cookie)); cls_rule_format(&rule->up.cr, result); ds_put_char(result, '\n'); @@ -3967,7 +3997,7 @@ trace_resubmit(struct action_xlate_ctx *ctx, struct rule_dpif *rule) ds_put_char(result, '\n'); trace_format_flow(result, ctx->recurse + 1, "Resubmitted flow", trace); - trace_format_rule(result, ctx->recurse + 1, rule); + trace_format_rule(result, ctx->table_id, ctx->recurse + 1, rule); } static void @@ -4054,8 +4084,8 @@ ofproto_unixctl_trace(struct unixctl_conn *conn, const char *args_, flow_format(&result, &flow); ds_put_char(&result, '\n'); - rule = rule_dpif_lookup(ofproto, &flow); - trace_format_rule(&result, 0, rule); + rule = rule_dpif_lookup(ofproto, &flow, 0); + trace_format_rule(&result, 0, 0, rule); if (rule) { struct ofproto_trace trace; struct ofpbuf *odp_actions; diff --git a/tests/automake.mk b/tests/automake.mk index eb6351f6..28b4e1d0 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -28,6 +28,7 @@ TESTSUITE_AT = \ tests/timeval.at \ tests/lockfile.at \ tests/reconnect.at \ + tests/ofproto-dpif.at \ tests/ofproto-macros.at \ tests/ofproto.at \ tests/ovsdb.at \ diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at new file mode 100644 index 00000000..846eccbd --- /dev/null +++ b/tests/ofproto-dpif.at @@ -0,0 +1,19 @@ +AT_BANNER([ofproto-dpif]) + +AT_SETUP([ofproto-dpif - resubmit]) +OFPROTO_START +AT_DATA([flows.txt], [dnl +table=0 in_port=1 priority=1000 icmp actions=output(10),resubmit(2),output(19),resubmit(3),output(21) +table=0 in_port=2 priority=1500 icmp actions=output(11),resubmit(,1),output(16),resubmit(2,1),output(18) +table=0 in_port=3 priority=2000 icmp actions=output(20) +table=1 in_port=1 priority=1000 icmp actions=output(12),resubmit(4,1),output(13),resubmit(3),output(15) +table=1 in_port=2 priority=1500 icmp actions=output(17),resubmit(,2) +table=1 in_port=3 priority=1500 icmp actions=output(14),resubmit(,2) +]) +AT_CHECK([ovs-ofctl add-flows br0 flows.txt]) +AT_CHECK([ovs-appctl -t test-openflowd ofproto/trace br0 'in_port(1),eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=1,tos=0),icmp(type=8,code=0)'], [0], [stdout]) +AT_CHECK([tail -1 stdout], [0], + [Datapath actions: 10,11,12,13,14,15,16,17,18,19,20,21 +]) +OFPROTO_STOP +AT_CLEANUP diff --git a/tests/ovs-ofctl.at b/tests/ovs-ofctl.at index 20dcc905..1edfb624 100644 --- a/tests/ovs-ofctl.at +++ b/tests/ovs-ofctl.at @@ -23,7 +23,9 @@ actions=bundle_load(eth_src,50,active_backup,ofport,NXM_NX_REG0[],slaves:1) actions=bundle_load(symmetric_l4,60,hrw,ofport,NXM_NX_REG0[0..15],slaves:2,3) actions=bundle_load(symmetric_l4,60,hrw,ofport,NXM_NX_REG0[0..30],slaves:) actions=output:1,bundle_load(eth_src,0,hrw,ofport,NXM_NX_REG0[16..31],slaves:1),output:2 +actions=resubmit:1,resubmit(2),resubmit(,3),resubmit(2,3) ]]) + AT_CHECK([ovs-ofctl parse-flows flows.txt ], [0], [stdout]) AT_CHECK([[sed 's/ (xid=0x[0-9a-fA-F]*)//' stdout]], [0], @@ -49,6 +51,7 @@ NXT_FLOW_MOD: ADD table:255 actions=bundle_load(eth_src,50,active_backup,ofport, NXT_FLOW_MOD: ADD table:255 actions=bundle_load(symmetric_l4,60,hrw,ofport,NXM_NX_REG0[0..15],slaves:2,3) NXT_FLOW_MOD: ADD table:255 actions=bundle_load(symmetric_l4,60,hrw,ofport,NXM_NX_REG0[0..30],slaves:) NXT_FLOW_MOD: ADD table:255 actions=output:1,bundle_load(eth_src,0,hrw,ofport,NXM_NX_REG0[16..31],slaves:1),output:2 +NXT_FLOW_MOD: ADD table:255 actions=resubmit:1,resubmit:2,resubmit(,3),resubmit(2,3) ]]) AT_CLEANUP diff --git a/tests/testsuite.at b/tests/testsuite.at index c22ece71..b394cb6f 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -60,6 +60,7 @@ m4_include([tests/timeval.at]) m4_include([tests/lockfile.at]) m4_include([tests/reconnect.at]) m4_include([tests/ofproto.at]) +m4_include([tests/ofproto-dpif.at]) m4_include([tests/ovsdb.at]) m4_include([tests/ovs-vsctl.at]) m4_include([tests/interface-reconfigure.at]) diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in index c59bca93..aa379694 100644 --- a/utilities/ovs-ofctl.8.in +++ b/utilities/ovs-ofctl.8.in @@ -655,10 +655,16 @@ only known to be implemented by Open vSwitch: .RS . .IP \fBresubmit\fB:\fIport\fR -Re-searches the OpenFlow flow table with the \fBin_port\fR field -replaced by \fIport\fR and executes the actions found, if any, in -addition to any other actions in this flow entry. Recursive -\fBresubmit\fR actions are ignored. +.IQ \fBresubmit\fB(\fR[\fIport\fR]\fB,\fR[\fItable\fR]\fB) +Re-searches this OpenFlow flow table (or the table whose number is +specified by \fItable\fR) with the \fBin_port\fR field replaced by +\fIport\fR (if \fIport\fR is specified) and executes the actions +found, if any, in addition to any other actions in this flow entry. +.IP +Recursive \fBresubmit\fR actions are obeyed up to an +implementation-defined maximum depth. Open vSwitch 1.0.1 and earlier +did not support recursion; Open vSwitch before 1.2.90 did not support +\fItable\fR. . .IP \fBset_tunnel\fB:\fIid\fR .IQ \fBset_tunnel64\fB:\fIid\fR