summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS3
-rw-r--r--include/openflow/nicira-ext.h36
-rw-r--r--lib/learn.c26
-rw-r--r--lib/ofp-actions.h2
-rw-r--r--ofproto/ofproto-provider.h7
-rw-r--r--ofproto/ofproto.c171
-rw-r--r--tests/learn.at138
-rw-r--r--utilities/ovs-ofctl.8.in10
8 files changed, 375 insertions, 18 deletions
diff --git a/NEWS b/NEWS
index 82a61e168..23d05232a 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,8 @@
Post-v2.3.0
---------------------
+ - The "learn" action supports a new flag "delete_learned" that causes
+ the learned flows to be deleted when the flow with the "learn" action
+ is deleted.
v2.3.0 - xx xxx xxxx
diff --git a/include/openflow/nicira-ext.h b/include/openflow/nicira-ext.h
index 767b98b0a..cb3413a8d 100644
--- a/include/openflow/nicira-ext.h
+++ b/include/openflow/nicira-ext.h
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013 Nicira, Inc.
+ * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Nicira, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -927,7 +927,7 @@ struct nx_action_learn {
ovs_be16 hard_timeout; /* Max time before discarding (seconds). */
ovs_be16 priority; /* Priority level of flow entry. */
ovs_be64 cookie; /* Cookie for new flow. */
- ovs_be16 flags; /* Either 0 or OFPFF_SEND_FLOW_REM. */
+ ovs_be16 flags; /* NX_LEARN_F_*. */
uint8_t table_id; /* Table to insert flow entry. */
uint8_t pad; /* Must be zero. */
ovs_be16 fin_idle_timeout; /* Idle timeout after FIN, if nonzero. */
@@ -937,6 +937,38 @@ struct nx_action_learn {
};
OFP_ASSERT(sizeof(struct nx_action_learn) == 32);
+/* Bits for 'flags' in struct nx_action_learn.
+ *
+ * If NX_LEARN_F_SEND_FLOW_REM is set, then the learned flows will have their
+ * OFPFF_SEND_FLOW_REM flag set.
+ *
+ * If NX_LEARN_F_DELETE_LEARNED is set, then removing this action will delete
+ * all the flows from the learn action's 'table_id' that have the learn
+ * action's 'cookie'. Important points:
+ *
+ * - The deleted flows include those created by this action, those created
+ * by other learn actions with the same 'table_id' and 'cookie', those
+ * created by flow_mod requests by a controller in the specified table
+ * with the specified cookie, and those created through any other
+ * means.
+ *
+ * - If multiple flows specify "learn" actions with
+ * NX_LEARN_F_DELETE_LEARNED with the same 'table_id' and 'cookie', then
+ * no deletion occurs until all of those "learn" actions are deleted.
+ *
+ * - Deleting a flow that contains a learn action is the most obvious way
+ * to delete a learn action. Modifying a flow's actions, or replacing it
+ * by a new flow, can also delete a learn action. Finally, replacing a
+ * learn action with NX_LEARN_F_DELETE_LEARNED with a learn action
+ * without that flag also effectively deletes the learn action and can
+ * trigger flow deletion.
+ *
+ * NX_LEARN_F_DELETE_LEARNED was added in Open vSwitch 2.4. */
+enum nx_learn_flags {
+ NX_LEARN_F_SEND_FLOW_REM = 1 << 0,
+ NX_LEARN_F_DELETE_LEARNED = 1 << 1,
+};
+
#define NX_LEARN_N_BITS_MASK 0x3ff
#define NX_LEARN_SRC_FIELD (0 << 13) /* Copy from field. */
diff --git a/lib/learn.c b/lib/learn.c
index 911dd04f8..c5797ebba 100644
--- a/lib/learn.c
+++ b/lib/learn.c
@@ -101,15 +101,9 @@ learn_from_openflow(const struct nx_action_learn *nal, struct ofpbuf *ofpacts)
learn->fin_idle_timeout = ntohs(nal->fin_idle_timeout);
learn->fin_hard_timeout = ntohs(nal->fin_hard_timeout);
- /* We only support "send-flow-removed" for now. */
- switch (ntohs(nal->flags)) {
- case 0:
- learn->flags = 0;
- break;
- case OFPFF_SEND_FLOW_REM:
- learn->flags = OFPUTIL_FF_SEND_FLOW_REM;
- break;
- default:
+ learn->flags = ntohs(nal->flags);
+ if (learn->flags & ~(NX_LEARN_F_SEND_FLOW_REM |
+ NX_LEARN_F_DELETE_LEARNED)) {
return OFPERR_OFPBAC_BAD_ARGUMENT;
}
@@ -321,7 +315,10 @@ learn_execute(const struct ofpact_learn *learn, const struct flow *flow,
fm->hard_timeout = learn->hard_timeout;
fm->buffer_id = UINT32_MAX;
fm->out_port = OFPP_NONE;
- fm->flags = learn->flags;
+ fm->flags = 0;
+ if (learn->flags & NX_LEARN_F_SEND_FLOW_REM) {
+ fm->flags |= OFPUTIL_FF_SEND_FLOW_REM;
+ }
fm->ofpacts = NULL;
fm->ofpacts_len = 0;
fm->delete_reason = OFPRR_DELETE;
@@ -582,7 +579,9 @@ learn_parse__(char *orig, char *arg, struct ofpbuf *ofpacts)
} else if (!strcmp(name, "cookie")) {
learn->cookie = htonll(strtoull(value, NULL, 0));
} else if (!strcmp(name, "send_flow_rem")) {
- learn->flags |= OFPFF_SEND_FLOW_REM;
+ learn->flags |= NX_LEARN_F_SEND_FLOW_REM;
+ } else if (!strcmp(name, "delete_learned")) {
+ learn->flags |= NX_LEARN_F_DELETE_LEARNED;
} else {
struct ofpact_learn_spec *spec;
char *error;
@@ -656,9 +655,12 @@ learn_format(const struct ofpact_learn *learn, struct ds *s)
if (learn->priority != OFP_DEFAULT_PRIORITY) {
ds_put_format(s, ",priority=%"PRIu16, learn->priority);
}
- if (learn->flags & OFPFF_SEND_FLOW_REM) {
+ if (learn->flags & NX_LEARN_F_SEND_FLOW_REM) {
ds_put_cstr(s, ",send_flow_rem");
}
+ if (learn->flags & NX_LEARN_F_DELETE_LEARNED) {
+ ds_put_cstr(s, ",delete_learned");
+ }
if (learn->cookie != 0) {
ds_put_format(s, ",cookie=%#"PRIx64, ntohll(learn->cookie));
}
diff --git a/lib/ofp-actions.h b/lib/ofp-actions.h
index 86522ab4d..8cb88629c 100644
--- a/lib/ofp-actions.h
+++ b/lib/ofp-actions.h
@@ -482,7 +482,7 @@ struct ofpact_learn {
uint16_t priority; /* Priority level of flow entry. */
uint8_t table_id; /* Table to insert flow entry. */
ovs_be64 cookie; /* Cookie for new flow. */
- enum ofputil_flow_mod_flags flags;
+ enum nx_learn_flags flags; /* NX_LEARN_F_*. */
uint16_t fin_idle_timeout; /* Idle timeout after FIN, if nonzero. */
uint16_t fin_hard_timeout; /* Hard timeout after FIN, if nonzero. */
diff --git a/ofproto/ofproto-provider.h b/ofproto/ofproto-provider.h
index 9c6b960d9..c8d70e615 100644
--- a/ofproto/ofproto-provider.h
+++ b/ofproto/ofproto-provider.h
@@ -96,6 +96,7 @@ struct ofproto {
/* Rules indexed on their cookie values, in all flow tables. */
struct hindex cookies OVS_GUARDED_BY(ofproto_mutex);
+ struct hmap learned_cookies OVS_GUARDED_BY(ofproto_mutex);
/* List of expirable flows, in all flow tables. */
struct list expirable OVS_GUARDED_BY(ofproto_mutex);
@@ -405,8 +406,12 @@ static inline bool rule_is_hidden(const struct rule *);
struct rule_actions {
/* Flags.
*
- * 'has_meter' is true if 'ofpacts' contains an OFPACT_METER action. */
+ * 'has_meter' is true if 'ofpacts' contains an OFPACT_METER action.
+ *
+ * 'has_learn_with_delete' is true if 'ofpacts' contains an OFPACT_LEARN
+ * action whose flags include NX_LEARN_F_DELETE_LEARNED. */
bool has_meter;
+ bool has_learn_with_delete;
/* Actions. */
uint32_t ofpacts_len; /* Size of 'ofpacts', in bytes. */
diff --git a/ofproto/ofproto.c b/ofproto/ofproto.c
index 660c308af..a3ab2763d 100644
--- a/ofproto/ofproto.c
+++ b/ofproto/ofproto.c
@@ -161,6 +161,10 @@ static void rule_criteria_require_rw(struct rule_criteria *,
bool can_write_readonly);
static void rule_criteria_destroy(struct rule_criteria *);
+static enum ofperr collect_rules_loose(struct ofproto *,
+ const struct rule_criteria *,
+ struct rule_collection *);
+
/* A packet that needs to be passed to rule_execute().
*
* (We can't do this immediately from ofopgroup_complete() because that holds
@@ -175,6 +179,37 @@ struct rule_execute {
static void run_rule_executes(struct ofproto *) OVS_EXCLUDED(ofproto_mutex);
static void destroy_rule_executes(struct ofproto *);
+struct learned_cookie {
+ union {
+ /* In struct ofproto's 'learned_cookies' hmap. */
+ struct hmap_node hmap_node OVS_GUARDED_BY(ofproto_mutex);
+
+ /* In 'dead_cookies' list when removed from hmap. */
+ struct list list_node;
+ } u;
+
+ /* Key. */
+ ovs_be64 cookie OVS_GUARDED_BY(ofproto_mutex);
+ uint8_t table_id OVS_GUARDED_BY(ofproto_mutex);
+
+ /* Number of references from "learn" actions.
+ *
+ * When this drops to 0, all of the flows in 'table_id' with the specified
+ * 'cookie' are deleted. */
+ int n OVS_GUARDED_BY(ofproto_mutex);
+};
+
+static const struct ofpact_learn *next_learn_with_delete(
+ const struct rule_actions *, const struct ofpact_learn *start);
+
+static void learned_cookies_inc(struct ofproto *, const struct rule_actions *)
+ OVS_REQUIRES(ofproto_mutex);
+static void learned_cookies_dec(struct ofproto *, const struct rule_actions *,
+ struct list *dead_cookies)
+ OVS_REQUIRES(ofproto_mutex);
+static void learned_cookies_flush(struct ofproto *, struct list *dead_cookies)
+ OVS_REQUIRES(ofproto_mutex);
+
/* ofport. */
static void ofport_destroy__(struct ofport *) OVS_EXCLUDED(ofproto_mutex);
static void ofport_destroy(struct ofport *);
@@ -477,6 +512,7 @@ ofproto_create(const char *datapath_name, const char *datapath_type,
ofproto->tables = NULL;
ofproto->n_tables = 0;
hindex_init(&ofproto->cookies);
+ hmap_init(&ofproto->learned_cookies);
list_init(&ofproto->expirable);
ofproto->connmgr = connmgr_create(ofproto, datapath_name, datapath_name);
guarded_list_init(&ofproto->rule_executes);
@@ -1263,6 +1299,9 @@ ofproto_destroy__(struct ofproto *ofproto)
ovs_assert(hindex_is_empty(&ofproto->cookies));
hindex_destroy(&ofproto->cookies);
+ ovs_assert(hmap_is_empty(&ofproto->learned_cookies));
+ hmap_destroy(&ofproto->learned_cookies);
+
free(ofproto->vlan_bitmap);
ofproto->ofproto_class->dealloc(ofproto);
@@ -2506,6 +2545,9 @@ rule_actions_create(const struct ofpact *ofpacts, size_t ofpacts_len)
actions->has_meter = ofpacts_get_meter(ofpacts, ofpacts_len) != 0;
memcpy(actions->ofpacts, ofpacts, ofpacts_len);
+ actions->has_learn_with_delete = (next_learn_with_delete(actions, NULL)
+ != NULL);
+
return actions;
}
@@ -2598,6 +2640,122 @@ rule_is_readonly(const struct rule *rule)
return (table->flags & OFTABLE_READONLY) != 0;
}
+static uint32_t
+hash_learned_cookie(ovs_be64 cookie_, uint8_t table_id)
+{
+ uint64_t cookie = (OVS_FORCE uint64_t) cookie_;
+ return hash_3words(cookie, cookie >> 32, table_id);
+}
+
+static void
+learned_cookies_update_one__(struct ofproto *ofproto,
+ const struct ofpact_learn *learn,
+ int delta, struct list *dead_cookies)
+ OVS_REQUIRES(ofproto_mutex)
+{
+ uint32_t hash = hash_learned_cookie(learn->cookie, learn->table_id);
+ struct learned_cookie *c;
+
+ HMAP_FOR_EACH_WITH_HASH (c, u.hmap_node, hash, &ofproto->learned_cookies) {
+ if (c->cookie == learn->cookie && c->table_id == learn->table_id) {
+ c->n += delta;
+ ovs_assert(c->n >= 0);
+
+ if (!c->n) {
+ hmap_remove(&ofproto->learned_cookies, &c->u.hmap_node);
+ list_push_back(dead_cookies, &c->u.list_node);
+ }
+
+ return;
+ }
+ }
+
+ ovs_assert(delta > 0);
+ c = xmalloc(sizeof *c);
+ hmap_insert(&ofproto->learned_cookies, &c->u.hmap_node, hash);
+ c->cookie = learn->cookie;
+ c->table_id = learn->table_id;
+ c->n = delta;
+}
+
+static const struct ofpact_learn *
+next_learn_with_delete(const struct rule_actions *actions,
+ const struct ofpact_learn *start)
+{
+ const struct ofpact *pos;
+
+ for (pos = start ? ofpact_next(&start->ofpact) : actions->ofpacts;
+ pos < ofpact_end(actions->ofpacts, actions->ofpacts_len);
+ pos = ofpact_next(pos)) {
+ if (pos->type == OFPACT_LEARN) {
+ const struct ofpact_learn *learn = ofpact_get_LEARN(pos);
+ if (learn->flags & NX_LEARN_F_DELETE_LEARNED) {
+ return learn;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+static void
+learned_cookies_update__(struct ofproto *ofproto,
+ const struct rule_actions *actions,
+ int delta, struct list *dead_cookies)
+ OVS_REQUIRES(ofproto_mutex)
+{
+ if (actions->has_learn_with_delete) {
+ const struct ofpact_learn *learn;
+
+ for (learn = next_learn_with_delete(actions, NULL); learn;
+ learn = next_learn_with_delete(actions, learn)) {
+ learned_cookies_update_one__(ofproto, learn, delta, dead_cookies);
+ }
+ }
+}
+
+static void
+learned_cookies_inc(struct ofproto *ofproto,
+ const struct rule_actions *actions)
+ OVS_REQUIRES(ofproto_mutex)
+{
+ learned_cookies_update__(ofproto, actions, +1, NULL);
+}
+
+static void
+learned_cookies_dec(struct ofproto *ofproto,
+ const struct rule_actions *actions,
+ struct list *dead_cookies)
+ OVS_REQUIRES(ofproto_mutex)
+{
+ learned_cookies_update__(ofproto, actions, -1, dead_cookies);
+}
+
+static void
+learned_cookies_flush(struct ofproto *ofproto, struct list *dead_cookies)
+ OVS_REQUIRES(ofproto_mutex)
+{
+ struct learned_cookie *c, *next;
+
+ LIST_FOR_EACH_SAFE (c, next, u.list_node, dead_cookies) {
+ struct rule_criteria criteria;
+ struct rule_collection rules;
+ struct match match;
+
+ match_init_catchall(&match);
+ rule_criteria_init(&criteria, c->table_id, &match, 0,
+ c->cookie, OVS_BE64_MAX, OFPP_ANY, OFPG_ANY);
+ rule_criteria_require_rw(&criteria, false);
+ collect_rules_loose(ofproto, &criteria, &rules);
+ delete_flows__(&rules, OFPRR_DELETE, NULL);
+ rule_criteria_destroy(&criteria);
+ rule_collection_destroy(&rules);
+
+ list_remove(&c->u.list_node);
+ free(c);
+ }
+}
+
static enum ofperr
handle_echo_request(struct ofconn *ofconn, const struct ofp_header *oh)
{
@@ -3904,6 +4062,7 @@ add_flow(struct ofproto *ofproto, struct ofputil_flow_mod *fm,
ofproto_rule_unref(rule);
return error;
}
+ learned_cookies_inc(ofproto, actions);
if (minimask_get_vid_mask(&rule->cr.match.mask) == VLAN_VID_MASK) {
if (ofproto->vlan_bitmap) {
@@ -3938,6 +4097,7 @@ modify_flows__(struct ofproto *ofproto, struct ofputil_flow_mod *fm,
const struct flow_mod_requester *req)
OVS_REQUIRES(ofproto_mutex)
{
+ struct list dead_cookies = LIST_INITIALIZER(&dead_cookies);
enum nx_flow_update_event event;
enum ofperr error;
size_t i;
@@ -4018,7 +4178,13 @@ modify_flows__(struct ofproto *ofproto, struct ofputil_flow_mod *fm,
ofmonitor_report(ofproto->connmgr, rule, event, 0,
req ? req->ofconn : NULL, req ? req->xid : 0);
}
+
+ if (change_actions) {
+ learned_cookies_inc(ofproto, rule_get_actions(rule));
+ learned_cookies_dec(ofproto, actions, &dead_cookies);
+ }
}
+ learned_cookies_flush(ofproto, &dead_cookies);
if (fm->buffer_id != UINT32_MAX && req) {
error = send_buffered_packet(req->ofconn, fm->buffer_id,
@@ -4112,11 +4278,13 @@ delete_flows__(const struct rule_collection *rules,
OVS_REQUIRES(ofproto_mutex)
{
if (rules->n) {
+ struct list dead_cookies = LIST_INITIALIZER(&dead_cookies);
struct ofproto *ofproto = rules->rules[0]->ofproto;
size_t i;
for (i = 0; i < rules->n; i++) {
struct rule *rule = rules->rules[i];
+ const struct rule_actions *actions = rule_get_actions(rule);
ofproto_rule_send_removed(rule, reason);
@@ -4124,7 +4292,10 @@ delete_flows__(const struct rule_collection *rules,
req ? req->ofconn : NULL, req ? req->xid : 0);
oftable_remove_rule(rule);
ofproto->ofproto_class->rule_delete(rule);
+
+ learned_cookies_dec(ofproto, actions, &dead_cookies);
}
+ learned_cookies_flush(ofproto, &dead_cookies);
ofmonitor_flush(ofproto->connmgr);
}
}
diff --git a/tests/learn.at b/tests/learn.at
index 3c304d10c..2ca58fc22 100644
--- a/tests/learn.at
+++ b/tests/learn.at
@@ -3,6 +3,9 @@ AT_BANNER([learning action])
AT_SETUP([learning action - parsing and formatting])
AT_DATA([flows.txt], [[
actions=learn()
+actions=learn(send_flow_rem)
+actions=learn(delete_learned)
+actions=learn(send_flow_rem,delete_learned)
actions=learn(NXM_OF_VLAN_TCI[0..11], NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[], output:NXM_OF_IN_PORT[], load:10->NXM_NX_REG0[5..10])
actions=learn(table=1,idle_timeout=10, hard_timeout=20, fin_idle_timeout=5, fin_hard_timeout=10, priority=10, cookie=0xfedcba9876543210, in_port=99,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG1[16..31])
]])
@@ -10,8 +13,11 @@ AT_CHECK([ovs-ofctl parse-flows flows.txt], [0],
[[usable protocols: any
chosen protocol: OpenFlow10-table_id
OFPT_FLOW_MOD (xid=0x1): ADD actions=learn(table=1)
-OFPT_FLOW_MOD (xid=0x2): ADD actions=learn(table=1,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[],load:0xa->NXM_NX_REG0[5..10])
-OFPT_FLOW_MOD (xid=0x3): ADD actions=learn(table=1,idle_timeout=10,hard_timeout=20,fin_idle_timeout=5,fin_hard_timeout=10,priority=10,cookie=0xfedcba9876543210,in_port=99,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG1[16..31])
+OFPT_FLOW_MOD (xid=0x2): ADD actions=learn(table=1,send_flow_rem)
+OFPT_FLOW_MOD (xid=0x3): ADD actions=learn(table=1,delete_learned)
+OFPT_FLOW_MOD (xid=0x4): ADD actions=learn(table=1,send_flow_rem,delete_learned)
+OFPT_FLOW_MOD (xid=0x5): ADD actions=learn(table=1,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[],load:0xa->NXM_NX_REG0[5..10])
+OFPT_FLOW_MOD (xid=0x6): ADD actions=learn(table=1,idle_timeout=10,hard_timeout=20,fin_idle_timeout=5,fin_hard_timeout=10,priority=10,cookie=0xfedcba9876543210,in_port=99,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG1[16..31])
]])
AT_CLEANUP
@@ -480,3 +486,131 @@ AT_CHECK([ovs-ofctl dump-flows br0 table=1 | ofctl_strip], [0],
])
OVS_VSWITCHD_STOP
AT_CLEANUP
+
+AT_SETUP([learning action - delete_learned feature])
+OVS_VSWITCHD_START
+
+# Add some initial flows and check that it was successful.
+AT_DATA([flows.txt], [dnl
+ reg0=0x1 actions=learn(delete_learned,cookie=0x123)
+ reg0=0x2 actions=learn(delete_learned,cookie=0x123)
+cookie=0x123, table=1, reg0=0x3 actions=drop
+cookie=0x123, table=1, reg0=0x4 actions=drop
+cookie=0x123, table=2, reg0=0x5 actions=drop
+cookie=0x234, table=1, reg0=0x6 actions=drop
+])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=1, reg0=0x3 actions=drop
+ cookie=0x123, table=1, reg0=0x4 actions=drop
+ cookie=0x123, table=2, reg0=0x5 actions=drop
+ cookie=0x234, table=1, reg0=0x6 actions=drop
+ reg0=0x1 actions=learn(table=1,delete_learned,cookie=0x123)
+ reg0=0x2 actions=learn(table=1,delete_learned,cookie=0x123)
+NXST_FLOW reply:
+])
+
+# Delete one of the learn actions. The learned flows should stay, since there
+# is another learn action with the identical target.
+AT_CHECK([ovs-ofctl del-flows br0 'table=0 reg0=1'])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=1, reg0=0x3 actions=drop
+ cookie=0x123, table=1, reg0=0x4 actions=drop
+ cookie=0x123, table=2, reg0=0x5 actions=drop
+ cookie=0x234, table=1, reg0=0x6 actions=drop
+ reg0=0x2 actions=learn(table=1,delete_learned,cookie=0x123)
+NXST_FLOW reply:
+])
+
+# Change the flow with the learn action by adding a second action. The learned
+# flows should stay because the learn action is still there.
+AT_CHECK([ovs-ofctl mod-flows br0 'table=0 reg0=2 actions=output:1,learn(delete_learned,cookie=0x123)'])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=1, reg0=0x3 actions=drop
+ cookie=0x123, table=1, reg0=0x4 actions=drop
+ cookie=0x123, table=2, reg0=0x5 actions=drop
+ cookie=0x234, table=1, reg0=0x6 actions=drop
+ reg0=0x2 actions=output:1,learn(table=1,delete_learned,cookie=0x123)
+NXST_FLOW reply:
+])
+
+# Change the flow with the learn action by replacing its learn action by one
+# with a different target. The (previous) learned flows disappear.
+AT_CHECK([ovs-ofctl mod-flows br0 'table=0 reg0=2 actions=learn(delete_learned,cookie=0x234)'])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=2, reg0=0x5 actions=drop
+ cookie=0x234, table=1, reg0=0x6 actions=drop
+ reg0=0x2 actions=learn(table=1,delete_learned,cookie=0x234)
+NXST_FLOW reply:
+])
+
+# Use add-flow to replace the flow with the learn action by one with the
+# same learn action and an extra action. The (new) learned flow remains.
+AT_CHECK([ovs-ofctl add-flow br0 'table=0 reg0=2 actions=learn(delete_learned,cookie=0x234),output:2'])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=2, reg0=0x5 actions=drop
+ cookie=0x234, table=1, reg0=0x6 actions=drop
+ reg0=0x2 actions=learn(table=1,delete_learned,cookie=0x234),output:2
+NXST_FLOW reply:
+])
+
+# Delete the flow with the learn action. The learned flow disappears too.
+AT_CHECK([ovs-ofctl del-flows br0 table=0])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=2, reg0=0x5 actions=drop
+NXST_FLOW reply:
+])
+
+# Add a new set of flows to check on a corner case: the learned flows
+# contain their own learn actions which cascade to further deletions.
+# This can't happen if the learned flows were actually created by a
+# learn action, since the learn action has very restricted action
+# support, but there's no restriction that the deleted flows were
+# created by a learn action.
+AT_DATA([flows.txt], [dnl
+ reg0=0x1 actions=learn(table=1,delete_learned,cookie=0x123)
+ reg0=0x2 actions=learn(table=2,delete_learned,cookie=0x234)
+cookie=0x123, table=1, reg0=0x3 actions=learn(table=3,delete_learned,cookie=0x345)
+cookie=0x234, table=2, reg0=0x3 actions=learn(table=4,delete_learned,cookie=0x456)
+cookie=0x345, table=3, reg0=0x4 actions=learn(table=5,delete_learned,cookie=0x567)
+cookie=0x456, table=4, reg0=0x5 actions=learn(table=5,delete_learned,cookie=0x567)
+cookie=0x567, table=5, reg0=0x6 actions=drop
+cookie=0x567, table=5, reg0=0x7 actions=drop
+cookie=0x567, table=5, reg0=0x8 actions=drop
+])
+AT_CHECK([ovs-ofctl del-flows br0])
+AT_CHECK([ovs-ofctl add-flows br0 flows.txt])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x123, table=1, reg0=0x3 actions=learn(table=3,delete_learned,cookie=0x345)
+ cookie=0x234, table=2, reg0=0x3 actions=learn(table=4,delete_learned,cookie=0x456)
+ cookie=0x345, table=3, reg0=0x4 actions=learn(table=5,delete_learned,cookie=0x567)
+ cookie=0x456, table=4, reg0=0x5 actions=learn(table=5,delete_learned,cookie=0x567)
+ cookie=0x567, table=5, reg0=0x6 actions=drop
+ cookie=0x567, table=5, reg0=0x7 actions=drop
+ cookie=0x567, table=5, reg0=0x8 actions=drop
+ reg0=0x1 actions=learn(table=1,delete_learned,cookie=0x123)
+ reg0=0x2 actions=learn(table=2,delete_learned,cookie=0x234)
+NXST_FLOW reply:
+])
+
+# Deleting the flow with reg0=1 should cascade to delete a few levels
+# of learned flows, but the ones with cookie=0x567 stick around
+# because of the flow with cookie=0x456.
+AT_CHECK([ovs-ofctl del-flows br0 'table=0 reg0=1'])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+ cookie=0x234, table=2, reg0=0x3 actions=learn(table=4,delete_learned,cookie=0x456)
+ cookie=0x456, table=4, reg0=0x5 actions=learn(table=5,delete_learned,cookie=0x567)
+ cookie=0x567, table=5, reg0=0x6 actions=drop
+ cookie=0x567, table=5, reg0=0x7 actions=drop
+ cookie=0x567, table=5, reg0=0x8 actions=drop
+ reg0=0x2 actions=learn(table=2,delete_learned,cookie=0x234)
+NXST_FLOW reply:
+])
+
+# Deleting the flow with reg0=2 should cascade to delete all the rest:
+AT_CHECK([ovs-ofctl del-flows br0 'table=0 reg0=2'])
+AT_CHECK([ovs-ofctl dump-flows br0 | ofctl_strip | sort], [0], [dnl
+NXST_FLOW reply:
+])
+OVS_VSWITCHD_STOP
+AT_CLEANUP
diff --git a/utilities/ovs-ofctl.8.in b/utilities/ovs-ofctl.8.in
index ad9fe783e..cc811ffc8 100644
--- a/utilities/ovs-ofctl.8.in
+++ b/utilities/ovs-ofctl.8.in
@@ -1491,6 +1491,16 @@ The table in which the new flow should be inserted. Specify a decimal
number between 0 and 254. The default, if \fBtable\fR is unspecified,
is table 1.
.
+.IP \fBdelete_learned\fR
+This flag enables deletion of the learned flows when the flow with the
+\fBlearn\fR action is removed. Specifically, when the last
+\fBlearn\fR action with this flag and particular \fBtable\fR and
+\fBcookie\fR values is removed, the switch deletes all of the flows in
+the specified table with the specified cookie.
+.
+.IP
+This flag was added in Open vSwitch 2.4.
+.
.IP \fIfield\fB=\fIvalue\fR
.IQ \fIfield\fB[\fIstart\fB..\fIend\fB]=\fIsrc\fB[\fIstart\fB..\fIend\fB]\fR
.IQ \fIfield\fB[\fIstart\fB..\fIend\fB]\fR