diff mbox series

[ovs-dev,v2,4/8] ovs-router: Add 'table=ID' parameter in ovs/route/show.

Message ID 20250723131253.3704827-5-dchumak@nvidia.com
State Changes Requested
Delegated to: Ilya Maximets
Headers show
Series ovs-router: Multi-table routing infrastructure. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed

Commit Message

Dima Chumak July 23, 2025, 1:12 p.m. UTC
Introduce an optional parameter to `ovs-appctl ovs/route/show` for
printing non-default routing tables:

  ovs-appctl ovs/route/show [table=ID|all]

Default usage is unchanged:

  ovs-appctl ovs/route/show
  Route Table:
  Cached: ::1/128 dev lo SRC ::1
  Cached: 127.0.0.0/8 dev lo SRC 127.0.0.1 local
  Cached: 10.7.7.0/24 dev br-phy1 SRC 10.7.7.17
  Cached: 0.0.0.0/0 dev eth1 GW 10.0.0.1 SRC 10.0.0.2

New usage with a specific table displays only the routes from that
table:

  ovs-appctl ovs/route/show table=10
  Route Table #10:
  Cached: 10.7.7.0/24 dev br-phy0 SRC 10.7.7.7

Special table 'all' displays all of the routes, the ones which are
coming from a non-default table have additional field 'table' displayed:

  ovs-appctl ovs/route/show table=all
  Cached: 10.7.7.0/24 dev br-phy1 SRC 10.7.7.17 table 20
  Cached: 10.7.7.0/24 dev br-phy0 SRC 10.7.7.7 table 10
  Cached: ::1/128 dev lo SRC ::1
  Cached: 127.0.0.0/8 dev lo SRC 127.0.0.1 local
  Cached: 10.7.7.0/24 dev br-phy1 SRC 10.7.7.17
  Cached: 0.0.0.0/0 dev eth1 GW 10.0.0.1 SRC 10.0.0.2

Signed-off-by: Dima Chumak <dchumak@nvidia.com>
---
 Documentation/howto/userspace-tunneling.rst |   2 +-
 NEWS                                        |   2 +
 lib/ovs-router.c                            | 180 ++++++++++++++------
 ofproto/ofproto-tnl-unixctl.man             |   9 +-
 tests/ovs-router.at                         |   4 +
 5 files changed, 142 insertions(+), 55 deletions(-)

Comments

Ilya Maximets Aug. 1, 2025, 8:43 p.m. UTC | #1
On 7/23/25 3:12 PM, Dima Chumak via dev wrote:
> Introduce an optional parameter to `ovs-appctl ovs/route/show` for
> printing non-default routing tables:
> 
>   ovs-appctl ovs/route/show [table=ID|all]
> 
> Default usage is unchanged:
> 
>   ovs-appctl ovs/route/show
>   Route Table:
>   Cached: ::1/128 dev lo SRC ::1
>   Cached: 127.0.0.0/8 dev lo SRC 127.0.0.1 local
>   Cached: 10.7.7.0/24 dev br-phy1 SRC 10.7.7.17
>   Cached: 0.0.0.0/0 dev eth1 GW 10.0.0.1 SRC 10.0.0.2
> 
> New usage with a specific table displays only the routes from that
> table:
> 
>   ovs-appctl ovs/route/show table=10
>   Route Table #10:
>   Cached: 10.7.7.0/24 dev br-phy0 SRC 10.7.7.7
> 
> Special table 'all' displays all of the routes, the ones which are
> coming from a non-default table have additional field 'table' displayed:
> 
>   ovs-appctl ovs/route/show table=all
>   Cached: 10.7.7.0/24 dev br-phy1 SRC 10.7.7.17 table 20
>   Cached: 10.7.7.0/24 dev br-phy0 SRC 10.7.7.7 table 10
>   Cached: ::1/128 dev lo SRC ::1
>   Cached: 127.0.0.0/8 dev lo SRC 127.0.0.1 local
>   Cached: 10.7.7.0/24 dev br-phy1 SRC 10.7.7.17
>   Cached: 0.0.0.0/0 dev eth1 GW 10.0.0.1 SRC 10.0.0.2
> 
> Signed-off-by: Dima Chumak <dchumak@nvidia.com>
> ---
>  Documentation/howto/userspace-tunneling.rst |   2 +-
>  NEWS                                        |   2 +
>  lib/ovs-router.c                            | 180 ++++++++++++++------
>  ofproto/ofproto-tnl-unixctl.man             |   9 +-
>  tests/ovs-router.at                         |   4 +
>  5 files changed, 142 insertions(+), 55 deletions(-)
> 
> diff --git a/Documentation/howto/userspace-tunneling.rst b/Documentation/howto/userspace-tunneling.rst
> index 31d82fd5e57a..1dd34cd2f5e4 100644
> --- a/Documentation/howto/userspace-tunneling.rst
> +++ b/Documentation/howto/userspace-tunneling.rst
> @@ -205,7 +205,7 @@ To add route::
>  
>  To see all routes configured::
>  
> -    $ ovs-appctl ovs/route/show
> +    $ ovs-appctl ovs/route/show [table=ID|all]
>  
>  To delete route::
>  
> diff --git a/NEWS b/NEWS
> index b5d565ecc73f..97554be8e6b4 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -24,6 +24,8 @@ v3.6.0 - xx xxx xxxx
>         core file size.
>     - ovs-appctl:
>       * Added JSON output support to the 'ovs/route/show' command.
> +     * 'ovs/route/show': added new option, table=[ID|all], to list routes from
> +       a specific OVS table or all routes from all tables.

We branched for 3.6 on July 16th.  These NEWS entries should go into
the Post-v3.6.0 section.

>     - ovs-vsctl:
>       * Now exits with error code 160 (ERROR_BAD_ARGUMENTS) on Windows and
>         65 (EX_DATAERR) on other platforms if it fails while waiting for
> diff --git a/lib/ovs-router.c b/lib/ovs-router.c
> index 2addc7b43d07..eaf688467389 100644
> --- a/lib/ovs-router.c
> +++ b/lib/ovs-router.c
> @@ -642,22 +642,15 @@ ovs_router_del(struct unixctl_conn *conn, int argc OVS_UNUSED,
>  }
>  
>  static void
> -ovs_router_show_json(struct json **routes)
> +ovs_router_show_json(struct json **json_entries, const struct classifier *cls,
> +                     uint32_t table)
>  {
> -    int n_rules = classifier_count(&cls_main);
> -    struct json **json_entries = NULL;
> +    int n_rules = classifier_count(cls);
> +    struct ds ds = DS_EMPTY_INITIALIZER;
>      struct ovs_router_entry *rt;
> -    struct ds ds;
>      int i = 0;
>  
> -    if (!n_rules) {
> -        goto out;
> -    }
> -
> -    json_entries = xmalloc(n_rules * sizeof *json_entries);
> -    ds_init(&ds);
> -
> -    CLS_FOR_EACH (rt, cr, &cls_main) {
> +    CLS_FOR_EACH (rt, cr, cls) {
>          bool user = rt->priority != rt->plen && !rt->local;
>          uint8_t plen = rt->plen;
>          struct json *json, *nh;
> @@ -673,6 +666,7 @@ ovs_router_show_json(struct json **routes)
>              plen -= 96;
>          }
>  
> +        json_object_put(json, "table", json_integer_create(table));
>          json_object_put(json, "user", json_boolean_create(user));
>          json_object_put(json, "local", json_boolean_create(rt->local));
>          json_object_put(json, "priority", json_integer_create(rt->priority));
> @@ -702,48 +696,53 @@ ovs_router_show_json(struct json **routes)
>      }
>  
>      ds_destroy(&ds);
> -
> -out:
> -    *routes = json_array_create(json_entries, i);
>  }
>  
>  static void
> -ovs_router_show_text(struct ds *ds)
> +ovs_router_show_text(struct ds *ds, const struct classifier *cls,
> +                     uint32_t table, bool show_header)
>  {
> -    struct classifier *std_cls[] = { &cls_local, &cls_main, &cls_default };
>      struct ovs_router_entry *rt;
>  
> -    ds_put_format(ds, "Route Table:\n");
> -    for (int i = 0; i < ARRAY_SIZE(std_cls); i++) {
> -        CLS_FOR_EACH (rt, cr, std_cls[i]) {
> -            uint8_t plen;
> -            if (rt->priority == rt->plen || rt->local) {
> -                ds_put_format(ds, "Cached: ");
> -            } else {
> -                ds_put_format(ds, "User: ");
> -            }
> -            ipv6_format_mapped(&rt->nw_addr, ds);
> -            plen = rt->plen;
> -            if (IN6_IS_ADDR_V4MAPPED(&rt->nw_addr)) {
> -                plen -= 96;
> -            }
> -            ds_put_format(ds, "/%"PRIu8, plen);
> -            if (rt->mark) {
> -                ds_put_format(ds, " MARK %"PRIu32, rt->mark);
> -            }
> +    if (show_header) {
> +        if (route_table_is_standard_id(table)) {
> +            ds_put_format(ds, "Route Table:\n");
> +        } else {
> +            ds_put_format(ds, "Route Table #%u:\n", table);

PRIu32

> +        }
> +    }
>  
> -            ds_put_format(ds, " dev %s", rt->output_netdev);
> -            if (ipv6_addr_is_set(&rt->gw)) {
> -                ds_put_format(ds, " GW ");
> -                ipv6_format_mapped(&rt->gw, ds);
> -            }
> -            ds_put_format(ds, " SRC ");
> -            ipv6_format_mapped(&rt->src_addr, ds);
> -            if (rt->local) {
> -                ds_put_format(ds, " local");
> -            }
> -            ds_put_format(ds, "\n");
> +    CLS_FOR_EACH (rt, cr, cls) {
> +        uint8_t plen;
> +        if (rt->priority == rt->plen || rt->local) {
> +            ds_put_format(ds, "Cached: ");
> +        } else {
> +            ds_put_format(ds, "User: ");
> +        }
> +        ipv6_format_mapped(&rt->nw_addr, ds);
> +        plen = rt->plen;
> +        if (IN6_IS_ADDR_V4MAPPED(&rt->nw_addr)) {
> +            plen -= 96;
> +        }
> +        ds_put_format(ds, "/%"PRIu8, plen);
> +        if (rt->mark) {
> +            ds_put_format(ds, " MARK %"PRIu32, rt->mark);
> +        }
> +
> +        ds_put_format(ds, " dev %s", rt->output_netdev);
> +        if (ipv6_addr_is_set(&rt->gw)) {
> +            ds_put_format(ds, " GW ");
> +            ipv6_format_mapped(&rt->gw, ds);
> +        }
> +        ds_put_format(ds, " SRC ");
> +        ipv6_format_mapped(&rt->src_addr, ds);
> +        if (rt->local) {
> +            ds_put_format(ds, " local");
>          }
> +        if (!route_table_is_standard_id(table) && !show_header) {
> +            ds_put_format(ds, " table %u", table);

PRIu32

> +        }
> +        ds_put_format(ds, "\n");
>      }
>  }
>  
> @@ -751,15 +750,94 @@ static void
>  ovs_router_show(struct unixctl_conn *conn, int argc OVS_UNUSED,
>                 const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED)
>  {
> +    struct ds ds = DS_EMPTY_INITIALIZER;
> +    struct classifier *cls = &cls_main;
> +    uint32_t table = CLS_MAIN;
> +
> +    if (argc > 1) {
> +        if (!strcmp(argv[1], "table=all")) {
> +            table = CLS_ALL;
> +        } else if (!ovs_scan(argv[1], "table=%"SCNi32, &table)) {

SCNu32 ?

> +            unixctl_command_reply_error(conn, "Invalid table format");
> +            return;
> +        }
> +    }
> +
> +    if (table != CLS_ALL) {
> +        cls = cls_find(table);
> +        if (!cls) {
> +            ds_put_format(&ds, "Invalid param, table '%s' not found", argv[1]);

Please, don't shorten the words in user-visible text, but also
the "Invalid param" part seems unnecessary.

> +            unixctl_command_reply_error(conn, ds_cstr_ro(&ds));
> +            ds_destroy(&ds);
> +            return;
> +        }
> +    }
> +
>      if (unixctl_command_get_output_format(conn) == UNIXCTL_OUTPUT_FMT_JSON) {
> -        struct json *routes;
> +        struct json *routes, **json_entries = NULL;
> +        struct json **cls_entries;
> +        size_t num_routes = 0;
> +
> +        if (table == CLS_ALL) {
> +            struct clsmap_node *node;
> +
> +            ovs_mutex_lock(&mutex);
> +
> +            HMAP_FOR_EACH (node, hash_node, &clsmap) {
> +                num_routes += node->cls.n_rules;
> +            }
>  
> -        ovs_router_show_json(&routes);
> +            num_routes += cls_main.n_rules;
> +            json_entries = xzalloc(num_routes * sizeof *json_entries);
> +            cls_entries = json_entries;
> +
> +            HMAP_FOR_EACH (node, hash_node, &clsmap) {
> +                ovs_router_show_json(cls_entries, &node->cls, node->table);

We have so many allocation inside, so it doesn't really matter if we save a
couple reallocs by counting the exact number of elements beforehand.  We should
just create an empty json array and pass it into ovs_router_show_json(), where
we can use json_array_add().  It will be much easier to understand what's going
on here without manual allocations and the pointer arithmetic.

> +                cls_entries += node->cls.n_rules;
> +            }
> +            ovs_router_show_json(cls_entries, &cls_main, CLS_MAIN);
> +
> +            ovs_mutex_unlock(&mutex);
> +        } else if (route_table_is_standard_id(table)) {
> +            num_routes += cls_local.n_rules + cls_main.n_rules +
> +                          cls_default.n_rules;
> +            json_entries = xzalloc(num_routes * sizeof *json_entries);
> +            cls_entries = json_entries;
> +
> +            ovs_router_show_json(cls_entries, &cls_local, CLS_LOCAL);
> +            cls_entries += cls_local.n_rules;
> +            ovs_router_show_json(cls_entries, &cls_main, CLS_MAIN);
> +            cls_entries += cls_main.n_rules;
> +            ovs_router_show_json(cls_entries, &cls_default, CLS_DEFAULT);
> +        } else {
> +            if (cls->n_rules) {
> +                num_routes = cls->n_rules;
> +                json_entries = xmalloc(num_routes * sizeof *json_entries);
> +                ovs_router_show_json(json_entries, cls, table);
> +            }
> +        }
> +
> +        routes = json_array_create(json_entries, num_routes);
>          unixctl_command_reply_json(conn, routes);
>      } else {
> -        struct ds ds = DS_EMPTY_INITIALIZER;
> +        if (table == CLS_ALL) {
> +            struct clsmap_node *node;
>  
> -        ovs_router_show_text(&ds);
> +            ovs_router_show_text(&ds, &cls_local, CLS_LOCAL, false);
> +            ovs_mutex_lock(&mutex);
> +            HMAP_FOR_EACH (node, hash_node, &clsmap) {
> +                ovs_router_show_text(&ds, &node->cls, node->table, false);
> +            }
> +            ovs_mutex_unlock(&mutex);
> +            ovs_router_show_text(&ds, &cls_main, CLS_MAIN, false);
> +            ovs_router_show_text(&ds, &cls_default, CLS_DEFAULT, false);
> +        } else if (route_table_is_standard_id(table)) {
> +            ovs_router_show_text(&ds, &cls_local, CLS_LOCAL, true);
> +            ovs_router_show_text(&ds, &cls_main, CLS_MAIN, false);
> +            ovs_router_show_text(&ds, &cls_default, CLS_DEFAULT, false);
> +        } else {
> +            ovs_router_show_text(&ds, cls, table, true);
> +        }
>          unixctl_command_reply(conn, ds_cstr(&ds));
>          ds_destroy(&ds);
>      }
> @@ -935,7 +1013,7 @@ ovs_router_init(void)
>                                   "ip/plen dev [gw] "
>                                   "[pkt_mark=mark] [src=src_ip]",
>                                   2, 5, ovs_router_add, NULL);
> -        unixctl_command_register("ovs/route/show", "", 0, 0,
> +        unixctl_command_register("ovs/route/show", "[table=ID|all]", 0, 1,
>                                   ovs_router_show, NULL);
>          unixctl_command_register("ovs/route/del", "ip/plen "
>                                   "[pkt_mark=mark]", 1, 2, ovs_router_del,
> diff --git a/ofproto/ofproto-tnl-unixctl.man b/ofproto/ofproto-tnl-unixctl.man
> index a801cfdccc5c..b9e4d99b2bf8 100644
> --- a/ofproto/ofproto-tnl-unixctl.man
> +++ b/ofproto/ofproto-tnl-unixctl.man
> @@ -7,9 +7,12 @@ Adds \fIip\fR/\fIplen\fR route to vswitchd routing table. \fIoutput_bridge\fR
>  needs to be OVS bridge name.  This command is useful if OVS cached
>  routes does not look right.
>  .
> -.IP "\fBovs/route/show\fR"
> -Print all routes in OVS routing table, This includes routes cached
> -from system routing table and user configured routes.
> +.IP "\fBovs/route/show [table=\fIID|all\fB]\fR"

The '|' should not be italic.

For some reson all the commands in this file do not close the
original bold after the command name ends while they should...

> +Print routes in OVS routing table. This includes routes cached
> +from system routing table and user configured routes. By default, the contents
> +of the main routing table is displayed, unless requested otherwise with

s/main table/all the default tables (local, main, default)/

> +\fItable\fR parameter. In this case the contents of a specific table ID or of
> +all routing tables is printed.
>  .
>  .IP "\fBovs/route/del ip/plen [pkt_mark=mark]\fR"
>  Delete ip/plen route from OVS routing table.
> diff --git a/tests/ovs-router.at b/tests/ovs-router.at
> index 641b780a582a..dac096cc1aec 100644
> --- a/tests/ovs-router.at
> +++ b/tests/ovs-router.at
> @@ -39,6 +39,7 @@ AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
>      "prefix": 24,
>      "prefsrc": "2.2.2.2",
>      "priority": 184,
> +    "table": 254,
>      "user": false},
>    {
>      "dst": "1.1.1.0",
> @@ -50,6 +51,7 @@ AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
>      "prefix": 24,
>      "prefsrc": "2.2.2.2",
>      "priority": 152,
> +    "table": 254,
>      "user": true},
>    {
>      "dst": "1.1.2.0",
> @@ -62,6 +64,7 @@ AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
>      "prefix": 24,
>      "prefsrc": "2.2.2.2",
>      "priority": 152,
> +    "table": 254,
>      "user": true},
>    {
>      "dst": "2.2.2.3",
> @@ -73,6 +76,7 @@ AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
>      "prefix": 32,
>      "prefsrc": "2.2.2.2",
>      "priority": 160,
> +    "table": 254,
>      "user": true}]]
>  ])
>  OVS_VSWITCHD_STOP
diff mbox series

Patch

diff --git a/Documentation/howto/userspace-tunneling.rst b/Documentation/howto/userspace-tunneling.rst
index 31d82fd5e57a..1dd34cd2f5e4 100644
--- a/Documentation/howto/userspace-tunneling.rst
+++ b/Documentation/howto/userspace-tunneling.rst
@@ -205,7 +205,7 @@  To add route::
 
 To see all routes configured::
 
-    $ ovs-appctl ovs/route/show
+    $ ovs-appctl ovs/route/show [table=ID|all]
 
 To delete route::
 
diff --git a/NEWS b/NEWS
index b5d565ecc73f..97554be8e6b4 100644
--- a/NEWS
+++ b/NEWS
@@ -24,6 +24,8 @@  v3.6.0 - xx xxx xxxx
        core file size.
    - ovs-appctl:
      * Added JSON output support to the 'ovs/route/show' command.
+     * 'ovs/route/show': added new option, table=[ID|all], to list routes from
+       a specific OVS table or all routes from all tables.
    - ovs-vsctl:
      * Now exits with error code 160 (ERROR_BAD_ARGUMENTS) on Windows and
        65 (EX_DATAERR) on other platforms if it fails while waiting for
diff --git a/lib/ovs-router.c b/lib/ovs-router.c
index 2addc7b43d07..eaf688467389 100644
--- a/lib/ovs-router.c
+++ b/lib/ovs-router.c
@@ -642,22 +642,15 @@  ovs_router_del(struct unixctl_conn *conn, int argc OVS_UNUSED,
 }
 
 static void
-ovs_router_show_json(struct json **routes)
+ovs_router_show_json(struct json **json_entries, const struct classifier *cls,
+                     uint32_t table)
 {
-    int n_rules = classifier_count(&cls_main);
-    struct json **json_entries = NULL;
+    int n_rules = classifier_count(cls);
+    struct ds ds = DS_EMPTY_INITIALIZER;
     struct ovs_router_entry *rt;
-    struct ds ds;
     int i = 0;
 
-    if (!n_rules) {
-        goto out;
-    }
-
-    json_entries = xmalloc(n_rules * sizeof *json_entries);
-    ds_init(&ds);
-
-    CLS_FOR_EACH (rt, cr, &cls_main) {
+    CLS_FOR_EACH (rt, cr, cls) {
         bool user = rt->priority != rt->plen && !rt->local;
         uint8_t plen = rt->plen;
         struct json *json, *nh;
@@ -673,6 +666,7 @@  ovs_router_show_json(struct json **routes)
             plen -= 96;
         }
 
+        json_object_put(json, "table", json_integer_create(table));
         json_object_put(json, "user", json_boolean_create(user));
         json_object_put(json, "local", json_boolean_create(rt->local));
         json_object_put(json, "priority", json_integer_create(rt->priority));
@@ -702,48 +696,53 @@  ovs_router_show_json(struct json **routes)
     }
 
     ds_destroy(&ds);
-
-out:
-    *routes = json_array_create(json_entries, i);
 }
 
 static void
-ovs_router_show_text(struct ds *ds)
+ovs_router_show_text(struct ds *ds, const struct classifier *cls,
+                     uint32_t table, bool show_header)
 {
-    struct classifier *std_cls[] = { &cls_local, &cls_main, &cls_default };
     struct ovs_router_entry *rt;
 
-    ds_put_format(ds, "Route Table:\n");
-    for (int i = 0; i < ARRAY_SIZE(std_cls); i++) {
-        CLS_FOR_EACH (rt, cr, std_cls[i]) {
-            uint8_t plen;
-            if (rt->priority == rt->plen || rt->local) {
-                ds_put_format(ds, "Cached: ");
-            } else {
-                ds_put_format(ds, "User: ");
-            }
-            ipv6_format_mapped(&rt->nw_addr, ds);
-            plen = rt->plen;
-            if (IN6_IS_ADDR_V4MAPPED(&rt->nw_addr)) {
-                plen -= 96;
-            }
-            ds_put_format(ds, "/%"PRIu8, plen);
-            if (rt->mark) {
-                ds_put_format(ds, " MARK %"PRIu32, rt->mark);
-            }
+    if (show_header) {
+        if (route_table_is_standard_id(table)) {
+            ds_put_format(ds, "Route Table:\n");
+        } else {
+            ds_put_format(ds, "Route Table #%u:\n", table);
+        }
+    }
 
-            ds_put_format(ds, " dev %s", rt->output_netdev);
-            if (ipv6_addr_is_set(&rt->gw)) {
-                ds_put_format(ds, " GW ");
-                ipv6_format_mapped(&rt->gw, ds);
-            }
-            ds_put_format(ds, " SRC ");
-            ipv6_format_mapped(&rt->src_addr, ds);
-            if (rt->local) {
-                ds_put_format(ds, " local");
-            }
-            ds_put_format(ds, "\n");
+    CLS_FOR_EACH (rt, cr, cls) {
+        uint8_t plen;
+        if (rt->priority == rt->plen || rt->local) {
+            ds_put_format(ds, "Cached: ");
+        } else {
+            ds_put_format(ds, "User: ");
+        }
+        ipv6_format_mapped(&rt->nw_addr, ds);
+        plen = rt->plen;
+        if (IN6_IS_ADDR_V4MAPPED(&rt->nw_addr)) {
+            plen -= 96;
+        }
+        ds_put_format(ds, "/%"PRIu8, plen);
+        if (rt->mark) {
+            ds_put_format(ds, " MARK %"PRIu32, rt->mark);
+        }
+
+        ds_put_format(ds, " dev %s", rt->output_netdev);
+        if (ipv6_addr_is_set(&rt->gw)) {
+            ds_put_format(ds, " GW ");
+            ipv6_format_mapped(&rt->gw, ds);
+        }
+        ds_put_format(ds, " SRC ");
+        ipv6_format_mapped(&rt->src_addr, ds);
+        if (rt->local) {
+            ds_put_format(ds, " local");
         }
+        if (!route_table_is_standard_id(table) && !show_header) {
+            ds_put_format(ds, " table %u", table);
+        }
+        ds_put_format(ds, "\n");
     }
 }
 
@@ -751,15 +750,94 @@  static void
 ovs_router_show(struct unixctl_conn *conn, int argc OVS_UNUSED,
                const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED)
 {
+    struct ds ds = DS_EMPTY_INITIALIZER;
+    struct classifier *cls = &cls_main;
+    uint32_t table = CLS_MAIN;
+
+    if (argc > 1) {
+        if (!strcmp(argv[1], "table=all")) {
+            table = CLS_ALL;
+        } else if (!ovs_scan(argv[1], "table=%"SCNi32, &table)) {
+            unixctl_command_reply_error(conn, "Invalid table format");
+            return;
+        }
+    }
+
+    if (table != CLS_ALL) {
+        cls = cls_find(table);
+        if (!cls) {
+            ds_put_format(&ds, "Invalid param, table '%s' not found", argv[1]);
+            unixctl_command_reply_error(conn, ds_cstr_ro(&ds));
+            ds_destroy(&ds);
+            return;
+        }
+    }
+
     if (unixctl_command_get_output_format(conn) == UNIXCTL_OUTPUT_FMT_JSON) {
-        struct json *routes;
+        struct json *routes, **json_entries = NULL;
+        struct json **cls_entries;
+        size_t num_routes = 0;
+
+        if (table == CLS_ALL) {
+            struct clsmap_node *node;
+
+            ovs_mutex_lock(&mutex);
+
+            HMAP_FOR_EACH (node, hash_node, &clsmap) {
+                num_routes += node->cls.n_rules;
+            }
 
-        ovs_router_show_json(&routes);
+            num_routes += cls_main.n_rules;
+            json_entries = xzalloc(num_routes * sizeof *json_entries);
+            cls_entries = json_entries;
+
+            HMAP_FOR_EACH (node, hash_node, &clsmap) {
+                ovs_router_show_json(cls_entries, &node->cls, node->table);
+                cls_entries += node->cls.n_rules;
+            }
+            ovs_router_show_json(cls_entries, &cls_main, CLS_MAIN);
+
+            ovs_mutex_unlock(&mutex);
+        } else if (route_table_is_standard_id(table)) {
+            num_routes += cls_local.n_rules + cls_main.n_rules +
+                          cls_default.n_rules;
+            json_entries = xzalloc(num_routes * sizeof *json_entries);
+            cls_entries = json_entries;
+
+            ovs_router_show_json(cls_entries, &cls_local, CLS_LOCAL);
+            cls_entries += cls_local.n_rules;
+            ovs_router_show_json(cls_entries, &cls_main, CLS_MAIN);
+            cls_entries += cls_main.n_rules;
+            ovs_router_show_json(cls_entries, &cls_default, CLS_DEFAULT);
+        } else {
+            if (cls->n_rules) {
+                num_routes = cls->n_rules;
+                json_entries = xmalloc(num_routes * sizeof *json_entries);
+                ovs_router_show_json(json_entries, cls, table);
+            }
+        }
+
+        routes = json_array_create(json_entries, num_routes);
         unixctl_command_reply_json(conn, routes);
     } else {
-        struct ds ds = DS_EMPTY_INITIALIZER;
+        if (table == CLS_ALL) {
+            struct clsmap_node *node;
 
-        ovs_router_show_text(&ds);
+            ovs_router_show_text(&ds, &cls_local, CLS_LOCAL, false);
+            ovs_mutex_lock(&mutex);
+            HMAP_FOR_EACH (node, hash_node, &clsmap) {
+                ovs_router_show_text(&ds, &node->cls, node->table, false);
+            }
+            ovs_mutex_unlock(&mutex);
+            ovs_router_show_text(&ds, &cls_main, CLS_MAIN, false);
+            ovs_router_show_text(&ds, &cls_default, CLS_DEFAULT, false);
+        } else if (route_table_is_standard_id(table)) {
+            ovs_router_show_text(&ds, &cls_local, CLS_LOCAL, true);
+            ovs_router_show_text(&ds, &cls_main, CLS_MAIN, false);
+            ovs_router_show_text(&ds, &cls_default, CLS_DEFAULT, false);
+        } else {
+            ovs_router_show_text(&ds, cls, table, true);
+        }
         unixctl_command_reply(conn, ds_cstr(&ds));
         ds_destroy(&ds);
     }
@@ -935,7 +1013,7 @@  ovs_router_init(void)
                                  "ip/plen dev [gw] "
                                  "[pkt_mark=mark] [src=src_ip]",
                                  2, 5, ovs_router_add, NULL);
-        unixctl_command_register("ovs/route/show", "", 0, 0,
+        unixctl_command_register("ovs/route/show", "[table=ID|all]", 0, 1,
                                  ovs_router_show, NULL);
         unixctl_command_register("ovs/route/del", "ip/plen "
                                  "[pkt_mark=mark]", 1, 2, ovs_router_del,
diff --git a/ofproto/ofproto-tnl-unixctl.man b/ofproto/ofproto-tnl-unixctl.man
index a801cfdccc5c..b9e4d99b2bf8 100644
--- a/ofproto/ofproto-tnl-unixctl.man
+++ b/ofproto/ofproto-tnl-unixctl.man
@@ -7,9 +7,12 @@  Adds \fIip\fR/\fIplen\fR route to vswitchd routing table. \fIoutput_bridge\fR
 needs to be OVS bridge name.  This command is useful if OVS cached
 routes does not look right.
 .
-.IP "\fBovs/route/show\fR"
-Print all routes in OVS routing table, This includes routes cached
-from system routing table and user configured routes.
+.IP "\fBovs/route/show [table=\fIID|all\fB]\fR"
+Print routes in OVS routing table. This includes routes cached
+from system routing table and user configured routes. By default, the contents
+of the main routing table is displayed, unless requested otherwise with
+\fItable\fR parameter. In this case the contents of a specific table ID or of
+all routing tables is printed.
 .
 .IP "\fBovs/route/del ip/plen [pkt_mark=mark]\fR"
 Delete ip/plen route from OVS routing table.
diff --git a/tests/ovs-router.at b/tests/ovs-router.at
index 641b780a582a..dac096cc1aec 100644
--- a/tests/ovs-router.at
+++ b/tests/ovs-router.at
@@ -39,6 +39,7 @@  AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
     "prefix": 24,
     "prefsrc": "2.2.2.2",
     "priority": 184,
+    "table": 254,
     "user": false},
   {
     "dst": "1.1.1.0",
@@ -50,6 +51,7 @@  AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
     "prefix": 24,
     "prefsrc": "2.2.2.2",
     "priority": 152,
+    "table": 254,
     "user": true},
   {
     "dst": "1.1.2.0",
@@ -62,6 +64,7 @@  AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
     "prefix": 24,
     "prefsrc": "2.2.2.2",
     "priority": 152,
+    "table": 254,
     "user": true},
   {
     "dst": "2.2.2.3",
@@ -73,6 +76,7 @@  AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
     "prefix": 32,
     "prefsrc": "2.2.2.2",
     "priority": 160,
+    "table": 254,
     "user": true}]]
 ])
 OVS_VSWITCHD_STOP