diff mbox series

[ovs-dev,1/3,v11] utilities: Cli commands for overlay port mirroring.

Message ID 20250422091024.21582-1-arukomoinikova@k2.cloud
State Accepted
Headers show
Series [ovs-dev,1/3,v11] utilities: Cli commands for overlay port mirroring. | expand

Checks

Context Check Description
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Rukomoinikova Aleksandra April 22, 2025, 9:10 a.m. UTC
Changes introduced in the NB/SB db schema:

1) new field for mirroring rules has been added to the Mirror table,
   allowing the definition of traffic reflection rules.

2) new table, Mirror Rule, has been created to filter remote
   overlay traffic.

3) new mirroring type, lport, has been introduced to encapsulate
   mirrored traffic to another OVN port.

4) ovn-nbctl commands for to create lport mirror and rules.

Signed-off-by: Alexandra Rukomoinikova <arukomoinikova@k2.cloud>
Signed-off-by: Vladislav Odintsov <vlodintsov@k2.cloud>
Co-authored-by: Vladislav Odintsov <vlodintsov@k2.cloud>
Tested-by: Ivan Burnin <iburnin@k2.cloud>
Acked-by: Numan Siddique <numans@ovn.org>
---
v10 --> v11: added Acked-by.
---
 ovn-nb.ovsschema      |  25 ++++-
 ovn-nb.xml            |  45 ++++++++-
 ovn-sb.ovsschema      |   7 +-
 ovn-sb.xml            |   2 +-
 tests/ovn-nbctl.at    |  99 +++++++++++++++++++-
 utilities/ovn-nbctl.c | 213 +++++++++++++++++++++++++++++++++++++++---
 6 files changed, 369 insertions(+), 22 deletions(-)
diff mbox series

Patch

diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
index ea71d09bd..f55930a2e 100644
--- a/ovn-nb.ovsschema
+++ b/ovn-nb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Northbound",
-    "version": "7.11.0",
-    "cksum": "2520708261 39034",
+    "version": "7.12.0",
+    "cksum": "2749576410 39903",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -364,13 +364,32 @@ 
                 "type": {"type": {"key": {"type": "string",
                                           "enum": ["set", ["gre",
                                                            "erspan",
-                                                           "local"]]}}},
+                                                           "local",
+                                                           "lport"]]}}},
                 "index": {"type": "integer"},
+                "mirror_rules": {
+                    "type": {
+                        "key": {
+                            "type": "uuid",
+                            "refTable": "Mirror_Rule",
+                            "refType": "strong"},
+                        "min": 0,
+                        "max": "unlimited"}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
             "indexes": [["name"]],
             "isRoot": true},
+        "Mirror_Rule": {
+            "columns": {
+                "match": {"type": "string"},
+                "action": {"type": {
+                    "key": {"type": "string",
+                            "enum": ["set", ["mirror", "skip"]]}}},
+                "priority": {"type": {"key": {"type": "integer",
+                                              "minInteger": 0,
+                                              "maxInteger": 32767}}}},
+            "isRoot": false},
         "Meter": {
             "columns": {
                 "name": {"type": "string"},
diff --git a/ovn-nb.xml b/ovn-nb.xml
index bf1f1628b..e416a1fba 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3325,7 +3325,7 @@  or
     <column name="type">
       <p>
         The value of this field specifies the mirror type - <code>gre</code>,
-        <code>erspan</code> or <code>local</code>.
+        <code>erspan</code>, <code>local</code> or <code>lport</code>.
       </p>
     </column>
 
@@ -3339,11 +3339,54 @@  or
       </p>
     </column>
 
+    <column name="mirror_rules">
+      <p>
+        The value of this field represents the mirror rule for filtering
+        mirror traffic.
+      </p>
+    </column>
+
     <column name="external_ids">
       See <em>External IDs</em> at the beginning of this document.
     </column>
   </table>
 
+  <table name="Mirror_Rule" title="Mirror rule entry">
+    <p>
+      Each row in this table represents a mirror rule that can be used
+      for filtering of <code>lport</code> mirrored traffic.
+    </p>
+
+    <column name="match">
+      <p>
+        The match expression, describing which packets should be executed
+        against Mirror Rule action. The Logical_Flow expression language is
+        used.
+      </p>
+    </column>
+
+    <column name="action">
+      <p>The action to take when the Mirror Rule matches:</p>
+      <ul>
+        <li>
+          <code>mirror</code>: Mirror the matched packet.
+        </li>
+
+        <li>
+          <code>skip</code>: Do not mirror matched packet.
+        </li>
+      </ul>
+    </column>
+
+    <column name="priority">
+      <p>
+        The Mirror Rule priority. Rule with higher priority takes precedence
+        over those with lower. A rule is uniquely identified by the priority
+        and match string.
+      </p>
+    </column>
+  </table>
+
   <table name="Meter" title="Meter entry">
     <p>
       Each row in this table represents a meter that can be used for QoS or
diff --git a/ovn-sb.ovsschema b/ovn-sb.ovsschema
index d7cfeaa8f..7eda3a5d9 100644
--- a/ovn-sb.ovsschema
+++ b/ovn-sb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Southbound",
-    "version": "21.0.0",
-    "cksum": "4028932437 34711",
+    "version": "21.1.0",
+    "cksum": "880279393 34779",
     "tables": {
         "SB_Global": {
             "columns": {
@@ -156,7 +156,8 @@ 
                 "type": {"type": {"key": {"type": "string",
                                           "enum": ["set", ["gre",
                                                            "erspan",
-                                                           "local"]]}}},
+                                                           "local",
+                                                           "lport"]]}}},
                 "index": {"type": "integer"},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 9777cc64a..d1d0085af 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -3081,7 +3081,7 @@  tcp.flags = RST;
     <column name="type">
       <p>
         The value of this field specifies the mirror type - <code>gre</code>,
-        <code>erspan</code> or <code>local</code>.
+        <code>erspan</code>, <code>local</code> or <code>lport</code>.
       </p>
     </column>
 
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index fc74e5c62..7129ba004 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -457,13 +457,15 @@  check ovn-nbctl ls-add sw0
 check ovn-nbctl lsp-add sw0 sw0-port1
 check ovn-nbctl lsp-add sw0 sw0-port2
 check ovn-nbctl lsp-add sw0 sw0-port3
+check ovn-nbctl lsp-add sw0 sw0-port4
+check ovn-nbctl mirror-add mirror-lport lport to-lport sw0-port1
 
 dnl Add duplicate mirror name
 AT_CHECK([ovn-nbctl mirror-add mirror1 gre 0 from-lport 10.10.10.5], [1], [], [stderr])
 AT_CHECK([grep 'already exists' stderr], [0], [ignore])
 
 dnl Attach invalid source port to mirror
-AT_CHECK([ovn-nbctl lsp-attach-mirror sw0-port4 mirror3], [1], [], [stderr])
+AT_CHECK([ovn-nbctl lsp-attach-mirror sw0-port5 mirror3], [1], [], [stderr])
 AT_CHECK([grep 'port name not found' stderr], [0], [ignore])
 
 dnl Attach source port to invalid mirror
@@ -479,6 +481,11 @@  dnl Attach one more source port to mirror
 check ovn-nbctl lsp-attach-mirror sw0-port3 mirror3
 check_column "$mirror3uuid" nb:Logical_Switch_Port mirror_rules name=sw0-port3
 
+mirror_lportuuid=$(fetch_column nb:Mirror _uuid name=mirror-lport)
+dnl Attach source port to lport mirror
+check ovn-nbctl lsp-attach-mirror sw0-port4 mirror-lport
+check_column "$mirror_lportuuid" nb:Logical_Switch_Port mirror_rules name=sw0-port4
+
 dnl Verify if multiple ports are attached to the same mirror properly
 AT_CHECK([ovn-nbctl mirror-list], [0], [dnl
 mirror-local:
@@ -486,6 +493,11 @@  mirror-local:
   Sink     :  10.10.10.3
   Filter   :  both
 
+mirror-lport:
+  Type     :  lport
+  Sink     :  sw0-port1
+  Filter   :  to-lport
+
 mirror1:
   Type     :  gre
   Sink     :  10.10.10.1
@@ -523,6 +535,11 @@  check_column "" nb:Logical_Switch_Port mirror_rules name=sw0-port1
 
 dnl Check if the mirror deleted properly
 AT_CHECK([ovn-nbctl mirror-list], [0], [dnl
+mirror-lport:
+  Type     :  lport
+  Sink     :  sw0-port1
+  Filter   :  to-lport
+
 mirror1:
   Type     :  gre
   Sink     :  10.10.10.1
@@ -537,9 +554,12 @@  mirror2:
 
 ])
 
-dnl Delete another mirror
+dnl Delete mirrors one more mirror
 check ovn-nbctl mirror-del mirror2
 
+dnl Delete one more mirror
+check ovn-nbctl mirror-del mirror-lport
+
 dnl Update the Sink address
 check ovn-nbctl set mirror . sink=192.168.1.13
 
@@ -559,6 +579,81 @@  AT_CHECK([ovn-nbctl mirror-list], [0], [dnl
 
 dnl ---------------------------------------------------------------------
 
+OVN_NBCTL_TEST([ovn_nbctl_mirrors_rules], [mirror_rules], [
+check ovn-nbctl ls-add sw0
+check ovn-nbctl lsp-add sw0 sw0-p1
+check ovn-nbctl mirror-add mirror1 lport from-lport sw0-p1
+check ovn-nbctl mirror-add mirror2 lport to-lport sw0-p1
+
+check ovn-nbctl mirror-rule-add mirror1 100 '1' mirror
+check ovn-nbctl mirror-rule-add mirror2 150 'ip' mirror
+
+dnl Add mirror rule for non-exist mirror name
+AT_CHECK([ovn-nbctl mirror-rule-add mirror5 150 'ip' allow], [1], [], [stderr])
+AT_CHECK([grep 'not found' stderr], [0], [ignore])
+
+dnl Add same mirror rule for mirror
+AT_CHECK([ovn-nbctl mirror-rule-add mirror2 150 'ip' mirror], [1], [], [stderr])
+AT_CHECK([grep 'exists' stderr], [0], [ignore])
+
+AT_CHECK([ovn-nbctl mirror-list], [0], [dnl
+mirror1:
+  Type     :  lport
+  Sink     :  sw0-p1
+  Filter   :  from-lport
+  Rules    :
+         100                              1          mirror
+
+mirror2:
+  Type     :  lport
+  Sink     :  sw0-p1
+  Filter   :  to-lport
+  Rules    :
+         150                             ip          mirror
+
+])
+
+dnl Add one more mirror rule to mirror
+check ovn-nbctl mirror-rule-add mirror2 200 'icmp' mirror
+
+check ovn-nbctl mirror-rule-add mirror2 250 'icmp' skip
+
+dnl Mirror rule attach mirror
+mirrorrule1uuid=$(fetch_column nb:Mirror_rule _uuid name=mirror1)
+check_column "$mirrorrule1uuid" nb:Mirror mirror_rule mirror_rule="$mirrorrule1uuid"
+
+dnl Remove the mirror rule by name
+check ovn-nbctl mirror-rule-del mirror1
+
+dnl Mirror rule dettach mirror
+mirrorr1uuid=$(fetch_column nb:Mirror _uuid name=mirror1)
+check_column "" nb:Mirror mirror_rule mirror_rule="mirrorr1uuid"
+
+dnl Delete mirror rule by priority
+check ovn-nbctl mirror-rule-del mirror2 200
+
+dnl Remove the mirror rule by priority and match
+check ovn-nbctl mirror-rule-del mirror2 250 'icmp'
+
+AT_CHECK([ovn-nbctl mirror-list], [0], [dnl
+mirror1:
+  Type     :  lport
+  Sink     :  sw0-p1
+  Filter   :  from-lport
+
+mirror2:
+  Type     :  lport
+  Sink     :  sw0-p1
+  Filter   :  to-lport
+  Rules    :
+         150                             ip          mirror
+
+])
+
+])
+
+dnl ---------------------------------------------------------------------
+
 OVN_NBCTL_TEST([ovn_nbctl_nats], [NATs], [
 AT_CHECK([ovn-nbctl lr-add lr0])
 AT_CHECK([ovn-nbctl lr-nat-add lr0 snatt 30.0.0.2 192.168.1.2], [1], [],
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index e94658371..aa970a4fb 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -298,9 +298,10 @@  QoS commands:\n\
   qos-list SWITCH           print QoS rules for SWITCH\n\
 \n\
 Mirror commands:\n\
-  mirror-add NAME TYPE [INDEX] FILTER {IP | MIRROR-ID} \n\
+  mirror-add NAME TYPE [INDEX] FILTER {IP | MIRROR-ID| TARGET-PORT} \n\
                             add a mirror with given name\n\
-                            specify TYPE 'gre', 'erspan', or 'local'\n\
+                            specify TYPE 'gre', 'erspan', 'local'\n\
+                                or 'lport'.\n\
                             specify the tunnel INDEX value\n\
                                 (indicates key if GRE\n\
                                  erpsan_idx if ERSPAN)\n\
@@ -309,8 +310,16 @@  Mirror commands:\n\
                             specify Sink / Destination i.e. Remote IP, or a\n\
                                 local interface with external-ids:mirror-id\n\
                                 matching MIRROR-ID\n\
+                                In case of lport type specify logical switch\n\
+                                port, which is a mirror target.\n\
   mirror-del [NAME]         remove mirrors\n\
   mirror-list               print mirrors\n\
+  mirror-rule-add MIRROR-NAME PRIORITY MATCH ACTION \n\
+                            add a mirror rule selection to given lport\n\
+                            mirror.\n\
+                            specify MATCH for selecting mirrored traffic.\n\
+                            specify ACTION 'mirror' or 'skip'.\n\
+  mirror-rule-del MIRROR-NAME [PRIORITY | MATCH] remove mirrors\n\
 \n\
 Meter commands:\n\
   [--fair]\n\
@@ -1821,11 +1830,11 @@  nbctl_lsp_attach_mirror(struct ctl_context *ctx)
         return;
     }
 
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
     /* Check if same mirror rule already exists for the lsp */
     for (size_t i = 0; i < lsp->n_mirror_rules; i++) {
         if (uuid_equals(&lsp->mirror_rules[i]->header_.uuid,
                         &mirror->header_.uuid)) {
-            bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
             if (!may_exist) {
                 ctl_error(ctx, "mirror %s is already attached to the "
                           "logical port %s.",
@@ -7772,16 +7781,18 @@  static char * OVS_WARN_UNUSED_RESULT
 parse_mirror_type(const char *arg, const char **type_p)
 {
     /* Validate type.  Only require the first letter. */
-    if (arg[0] == 'g') {
+    if (!strcmp(arg, "gre")) {
         *type_p = "gre";
-    } else if (arg[0] == 'e') {
+    } else if (!strcmp(arg, "erspan")) {
         *type_p = "erspan";
-    } else if (arg[0] == 'l') {
+    } else if (!strcmp(arg, "local")) {
         *type_p = "local";
+    } else if (!strcmp(arg, "lport")) {
+        *type_p = "lport";
     } else {
         *type_p = NULL;
         return xasprintf("%s: type must be \"gre\", "
-                         "\"erspan\", or \"local\"", arg);
+                         "\"erspan\", or \"local\" or \"lport\"", arg);
     }
     return NULL;
 }
@@ -7794,6 +7805,7 @@  nbctl_pre_mirror_add(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_index);
     ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_sink);
     ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_type);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_logical_switch_port_col_name);
 }
 
 static void
@@ -7818,14 +7830,17 @@  nbctl_mirror_add(struct ctl_context *ctx)
         }
     }
 
-    /* Type - gre/erspan/local */
+    /* Type - gre/erspan/local/lport */
     error = parse_mirror_type(ctx->argv[pos++], &type);
     if (error) {
         ctx->error = error;
         return;
     }
 
-    if (strcmp(type, "local")) {
+    int is_local = !strcmp(type, "local");
+    int is_lport = !strcmp(type, "lport");
+
+    if (!is_local && !is_lport) {
         /* tunnel index / GRE key / ERSPAN idx */
         if (!str_to_long(ctx->argv[pos++], 10, (long int *) &index)) {
             ctl_error(ctx, "Invalid Index");
@@ -7844,7 +7859,7 @@  nbctl_mirror_add(struct ctl_context *ctx)
     sink = ctx->argv[pos++];
 
     /* check if it is a valid ip unless it is type 'local' */
-    if (strcmp(type, "local")) {
+    if (!is_local && !is_lport) {
         char *new_sink_ip = normalize_ipv4_addr_str(sink);
         if (!new_sink_ip) {
             new_sink_ip = normalize_ipv6_addr_str(sink);
@@ -7857,6 +7872,21 @@  nbctl_mirror_add(struct ctl_context *ctx)
         free(new_sink_ip);
     }
 
+    /* Check if it is an existing port for lport mirror type. */
+    if (is_lport) {
+        const struct nbrec_logical_switch_port *lsp;
+        error = lsp_by_name_or_uuid(ctx, sink, false, &lsp);
+        if (error) {
+            ctx->error = error;
+            return;
+        }
+
+        if (!lsp) {
+            VLOG_WARN("Attaching target to non existing port with name %s.",
+                      sink);
+        }
+    }
+
     /* Create the mirror. */
     struct nbrec_mirror *mirror = nbrec_mirror_insert(ctx->txn);
     nbrec_mirror_set_name(mirror, name);
@@ -7896,6 +7926,143 @@  nbctl_mirror_del(struct ctl_context *ctx)
     }
 }
 
+static int
+rule_cmp(const void *mirror1_, const void *mirror2_)
+{
+    const struct nbrec_mirror_rule *const *mirror_1 = mirror1_;
+    const struct nbrec_mirror_rule *const *mirror_2 = mirror2_;
+
+    const struct nbrec_mirror_rule *mirror1 = *mirror_1;
+    const struct nbrec_mirror_rule *mirror2 = *mirror_2;
+
+    int result =  mirror1->priority - mirror2->priority;
+    if (result) {
+        return result;
+    }
+
+    result = strcmp(mirror1->match, mirror2->match);
+    if (result) {
+        return result;
+    }
+
+    return strcmp(mirror1->action, mirror2->action);
+}
+
+static void
+nbctl_pre_mirror_rule_add(struct ctl_context *ctx)
+{
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_name);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_mirror_rules);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_match);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_action);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_priority);
+}
+
+static void
+nbctl_mirror_rule_add(struct ctl_context *ctx)
+{
+    const struct nbrec_mirror_rule *mirror_rule;
+    const struct nbrec_mirror *mirror;
+    int64_t priority = 0;
+    char *error;
+
+    error = mirror_by_name_or_uuid(ctx, ctx->argv[1], true, &mirror);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    error = parse_priority(ctx->argv[2], &priority);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    const char *action = ctx->argv[4];
+    if (strcmp(action, "mirror") && strcmp(action, "skip")) {
+        ctl_error(ctx, "%s: action must be one of \"mirror\", \"skip\"",
+                  action);
+    }
+
+    mirror_rule = nbrec_mirror_rule_insert(ctx->txn);
+    nbrec_mirror_rule_set_match(mirror_rule, ctx->argv[3]);
+    nbrec_mirror_rule_set_action(mirror_rule, action);
+    nbrec_mirror_rule_set_priority(mirror_rule, priority);
+
+    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
+    /* Check if same mirror rule exists for this mirror. */
+    for (size_t i = 0; i < mirror->n_mirror_rules; i++) {
+        if (!rule_cmp(&mirror_rule, &mirror->mirror_rules[i])) {
+            if (!may_exist) {
+                ctl_error(ctx, "Same mirror-rule already exists on the "
+                          "mirror %s.", ctx->argv[1]);
+            } else {
+                nbrec_mirror_rule_delete(mirror_rule);
+            }
+            return;
+        }
+    }
+
+    /* Insert mirror rule to mirror. */
+    nbrec_mirror_update_mirror_rules_addvalue(mirror, mirror_rule);
+}
+
+static void
+nbctl_pre_mirror_rule_del(struct ctl_context *ctx)
+{
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_name);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_mirror_rules);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_action);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_priority);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_match);
+}
+
+static void
+nbctl_mirror_rule_del(struct ctl_context *ctx)
+{
+    const struct nbrec_mirror *mirror = NULL;
+    int64_t priority = 0;
+    char *error = NULL;
+
+    error = mirror_by_name_or_uuid(ctx, ctx->argv[1], true, &mirror);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    if (ctx->argc == 2) {
+        for (size_t i = 0; i < mirror->n_mirror_rules; i++) {
+            nbrec_mirror_update_mirror_rules_delvalue(mirror,
+                                    mirror->mirror_rules[i]);
+        }
+        return;
+    }
+
+    error = parse_priority(ctx->argv[2], &priority);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    if (ctx->argc == 3) {
+        for (size_t i = 0; i < mirror->n_mirror_rules; i++) {
+            struct nbrec_mirror_rule *rule = mirror->mirror_rules[i];
+            if (priority == rule->priority) {
+                nbrec_mirror_update_mirror_rules_delvalue(mirror, rule);
+                return;
+            }
+        }
+    }
+
+    for (size_t i = 0; i < mirror->n_mirror_rules; i++) {
+        struct nbrec_mirror_rule *rule = mirror->mirror_rules[i];
+        if (priority == rule->priority && !strcmp(ctx->argv[3],
+            rule->match)) {
+            nbrec_mirror_update_mirror_rules_delvalue(mirror, rule);
+        }
+    }
+}
+
 static void
 nbctl_pre_mirror_list(struct ctl_context *ctx)
 {
@@ -7905,6 +8072,10 @@  nbctl_pre_mirror_list(struct ctl_context *ctx)
     ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_index);
     ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_sink);
     ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_type);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_col_mirror_rules);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_action);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_match);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_mirror_rule_col_priority);
 }
 
 static void
@@ -7931,15 +8102,27 @@  nbctl_mirror_list(struct ctl_context *ctx)
 
     for (size_t i = 0; i < n_mirrors; i++) {
         mirror = mirrors[i];
+        bool is_lport = !strcmp(mirror->type, "lport");
+        bool is_local = !strcmp(mirror->type, "local");
+
         ds_put_format(&ctx->output, "%s:\n", mirror->name);
         /* print all the values */
         ds_put_format(&ctx->output, "  Type     :  %s\n", mirror->type);
         ds_put_format(&ctx->output, "  Sink     :  %s\n", mirror->sink);
         ds_put_format(&ctx->output, "  Filter   :  %s\n", mirror->filter);
-        if (strcmp(mirror->type, "local")) {
+        if (!is_local && !is_lport) {
             ds_put_format(&ctx->output, "  Index/Key:  %"PRId64"\n",
                           mirror->index);
         }
+        if (mirror->n_mirror_rules) {
+            ds_put_cstr(&ctx->output, "  Rules    :\n");
+            for (size_t j = 0; j < mirror->n_mirror_rules; j++) {
+                ds_put_format(&ctx->output, "       %5"PRId64" %30s %15s\n",
+                              mirror->mirror_rules[j]->priority,
+                              mirror->mirror_rules[j]->match,
+                              mirror->mirror_rules[j]->action);
+            }
+        }
         ds_put_cstr(&ctx->output, "\n");
     }
 
@@ -8044,13 +8227,19 @@  static const struct ctl_command_syntax nbctl_commands[] = {
 
     /* mirror commands. */
     { "mirror-add", 4, 5,
-      "NAME TYPE INDEX FILTER IP",
+      "NAME TYPE INDEX FILTER SINK",
       nbctl_pre_mirror_add, nbctl_mirror_add, NULL, "--may-exist", RW },
     { "mirror-del", 0, 1, "[NAME]",
       nbctl_pre_mirror_del, nbctl_mirror_del, NULL, "", RW },
     { "mirror-list", 0, 0, "", nbctl_pre_mirror_list, nbctl_mirror_list,
       NULL, "", RO },
 
+    /* Mirror rule commands. */
+    { "mirror-rule-add", 4, 4, "MIRROR-NAME PRIORITY MATCH ACTION",
+      nbctl_pre_mirror_rule_add, nbctl_mirror_rule_add, NULL, "", RW},
+    { "mirror-rule-del", 1, 3, "MIRROR-NAME [PRIORITY MATCH]",
+      nbctl_pre_mirror_rule_del, nbctl_mirror_rule_del, NULL, "", RW },
+
     /* meter commands. */
     { "meter-add", 4, 5, "NAME ACTION RATE UNIT [BURST]", nbctl_pre_meter_add,
       nbctl_meter_add, NULL, "--fair,--may-exist", RW },