On 7/2/26 7:26 PM, Takeru Hayasaka wrote:
> Previously fdb/show required exactly one bridge argument.  This change
> lets it accept several bridge names in a single call, and show all
> bridges when no argument is given.  This reduces the number of unixctl
> round-trips for clients that poll FDB entries across many bridges
> (e.g. EVPN agents managing multiple VNIs), while still allowing a
> client to query only the subset of bridges it cares about.
> 
> When more than one bridge is shown, the entries are grouped under a
> "Bridge" column, sized to the widest bridge name shown, that prints
> each bridge name only on its first row; a bridge with no entries is
> listed as a name-only row so it stays visible.  JSON output is now an
> object keyed by bridge name whose values are arrays of that bridge's
> entries, e.g. {"br0": [...], "br1": [...]}; this replaces the bare
> entries array previously returned for a single bridge.  Single-bridge
> text output is unchanged.
> 
> Signed-off-by: Takeru Hayasaka <[email protected]>

Thanks for the update!  The patch looks good to me in general.
See a couple small comments below.

Best regards, Ilya Maximets.

> ---
> v4:
> - Rebased on current master; the NEWS entry now lives under the existing
>   "ovs-appctl" section.  No functional changes.
> v3:
> - No arguments now shows all bridges, matching commands like fdb/flush
>   (suggested by Ilya Maximets); one or more bridge names still restricts
>   output to just those bridges.
> - Replaced the v2 "bridge <name>" header line with a "Bridge" column,
>   added only when more than one bridge is shown, so single-bridge output
>   is byte-for-byte identical to before.  This reverts the test churn
>   introduced in v2 (including tests/stp.at).
> - The "Bridge" column is sized to the widest bridge name shown and
>   groups each bridge's entries, printing the name only on the bridge's
>   first row.  A bridge with no entries is listed as a name-only row, so
>   that (as in JSON) an existing-but-empty bridge stays visible in a
>   multi-bridge listing.  Added tests covering long names, a bridge with
>   multiple entries, and an empty bridge.
> - JSON output is now an object keyed by bridge name ({"br0": [...]})
>   rather than an array of {"bridge", "entries"} objects.
> - Trimmed the NEWS entry to a single item.
> - Updated the ovs-vswitchd(8) man page to match the above.
> v2:
> - Moved the "bridge <name>" prefixing into the per-bridge text and JSON
>   helpers (suggested by Mike Pattrick).
> - Updated the unixctl usage hint and the ovs-vswitchd(8) man page.
> - Added tests for an empty-FDB JSON response and for the error path when
>   a requested bridge does not exist.
> 
>  NEWS                       |   2 +
>  ofproto/ofproto-dpif.c     |  84 ++++++++++++++++---
>  tests/ofproto-dpif.at      | 162 +++++++++++++++++++++++++++++--------
>  vswitchd/ovs-vswitchd.8.in |  10 ++-
>  4 files changed, 211 insertions(+), 47 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index c9331321c0ad..608da24ca928 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -16,6 +16,8 @@ Post-v3.7.0
>       * 'dpif-netdev/pmd-stats-show' command was removed in favor of the more
>         informative and better structured 'dpif-netdev/pmd-perf-show', which
>         now also provides statistics for the "main" thread.
> +     * "fdb/show" now accepts multiple bridge names, and shows all bridges
> +       when no bridge is specified.
>     - Python:
>       * The ovstest Python module is now removed.  It was only used by the
>         ovs-test and ovs-l3ping utilities.
> diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c
> index 729bc6f1d4ac..0c970b55fd27 100644
> --- a/ofproto/ofproto-dpif.c
> +++ b/ofproto/ofproto-dpif.c
> @@ -6165,11 +6165,11 @@ ofbundle_get_a_port(const struct ofbundle *bundle)
>  
>  static void
>  ofproto_unixctl_fdb_show_text(const struct ofproto_dpif *ofproto,
> -                              struct ds *ds)
> +                              struct ds *ds, int name_width)

May be better to call it 'br_width', as we use 'name' for port names
inside the function.

>  {
>      const struct mac_entry *e;
> +    bool first = true;
>  
> -    ds_put_cstr(ds, " port  VLAN  MAC                Age\n");
>      ovs_rwlock_rdlock(&ofproto->ml->rwlock);
>      LIST_FOR_EACH (e, lru_node, &ofproto->ml->lrus) {
>          struct ofbundle *bundle = mac_entry_get_port(ofproto->ml, e);
> @@ -6178,6 +6178,12 @@ ofproto_unixctl_fdb_show_text(const struct 
> ofproto_dpif *ofproto,
>  
>          ofputil_port_to_string(ofbundle_get_a_port(bundle)->up.ofp_port,
>                  NULL, name, sizeof name);
> +        if (name_width) {
> +            /* Print the bridge name only on its first entry so that a
> +             * bridge's rows read as a single group. */
> +            ds_put_format(ds, " %-*s", name_width,
> +                          first ? ofproto->up.name : "");
> +        }
>          ds_put_format(ds, "%5s  %4d  "ETH_ADDR_FMT"  ",
>                  name, e->vlan, ETH_ADDR_ARGS(e->mac));
>          if (MAC_ENTRY_AGE_STATIC_ENTRY == age) {
> @@ -6185,6 +6191,12 @@ ofproto_unixctl_fdb_show_text(const struct 
> ofproto_dpif *ofproto,
>          } else {
>              ds_put_format(ds, "%3d\n", age);
>          }
> +        first = false;
> +    }
> +    if (name_width && first) {
> +        /* Bridge has no entries; still show its name so that it stays
> +         * visible in a multi-bridge listing, matching JSON output. */
> +        ds_put_format(ds, " %s\n", ofproto->up.name);
>      }
>      ovs_rwlock_unlock(&ofproto->ml->rwlock);
>  }
> @@ -6233,28 +6245,74 @@ done_unlock:
>  }
>  
>  static void
> -ofproto_unixctl_fdb_show(struct unixctl_conn *conn, int argc OVS_UNUSED,
> -                          const char *argv[] OVS_UNUSED, void *aux 
> OVS_UNUSED)
> +ofproto_unixctl_fdb_show(struct unixctl_conn *conn, int argc,
> +                          const char *argv[], void *aux OVS_UNUSED)
>  {
> -    const struct ofproto_dpif *ofproto = 
> ofproto_dpif_lookup_by_name(argv[1]);
> +    const struct ofproto_dpif **ofprotos;
> +    size_t n = 0;
>  
> -    if (!ofproto) {
> -        unixctl_command_reply_error(conn, "no such bridge");
> -        return;
> +    if (argc > 1) {
> +        /* Validate all requested bridges up front so that a bad name
> +         * produces an error with no partial output. */
> +        ofprotos = xmalloc((argc - 1) * sizeof *ofprotos);
> +        for (int i = 1; i < argc; i++) {
> +            const struct ofproto_dpif *ofproto =
> +                ofproto_dpif_lookup_by_name(argv[i]);
> +
> +            if (!ofproto) {
> +                unixctl_command_reply_error(conn, "no such bridge");

Would be good to print out the argument that triggered the error, i.e.
which bridge doesn't exist.

> +                free(ofprotos);
> +                return;
> +            }
> +            ofprotos[n++] = ofproto;
> +        }
> +    } else {
> +        /* No bridge specified: show all of them. */
> +        struct ofproto_dpif *ofproto;
> +
> +        ofprotos = xmalloc(hmap_count(&all_ofproto_dpifs_by_name)
> +                           * sizeof *ofprotos);
> +        HMAP_FOR_EACH (ofproto, all_ofproto_dpifs_by_name_node,
> +                       &all_ofproto_dpifs_by_name) {
> +            ofprotos[n++] = ofproto;
> +        }
>      }
>  
>      if (unixctl_command_get_output_format(conn) == UNIXCTL_OUTPUT_FMT_JSON) {
> -        struct json *fdb_entries;
> +        struct json *obj = json_object_create();
>  
> -        ofproto_unixctl_fdb_show_json(ofproto, &fdb_entries);
> -        unixctl_command_reply_json(conn, fdb_entries);
> +        for (size_t i = 0; i < n; i++) {
> +            struct json *entries;
> +
> +            ofproto_unixctl_fdb_show_json(ofprotos[i], &entries);
> +            json_object_put(obj, ofprotos[i]->up.name, entries);
> +        }
> +
> +        unixctl_command_reply_json(conn, obj);
>      } else {
>          struct ds ds = DS_EMPTY_INITIALIZER;
> +        int name_width = 0;
> +
> +        if (n > 1) {
> +            name_width = strlen("Bridge");
> +            for (size_t i = 0; i < n; i++) {
> +                int len = strlen(ofprotos[i]->up.name);

We don't really need this variable.  The strlen can be put directly
into the MAX, saving 2 lines.

> +
> +                name_width = MAX(name_width, len);
> +            }
> +            ds_put_format(&ds, " %-*s", name_width, "Bridge");
> +        }
> +        ds_put_cstr(&ds, " port  VLAN  MAC                Age\n");

There are 2 spaces at least between column names in the header, but
there is only one reserved between the 'Bridge' and the 'port'.  It
will look better if we have 2 here as well, e.g. by increasing the
name_width by one at the end.

It may be better to also convert 'port' into 'Port', so all the columns
look more similar.

> +
> +        for (size_t i = 0; i < n; i++) {
> +            ofproto_unixctl_fdb_show_text(ofprotos[i], &ds, name_width);
> +        }
>  
> -        ofproto_unixctl_fdb_show_text(ofproto, &ds);
>          unixctl_command_reply(conn, ds_cstr(&ds));
>          ds_destroy(&ds);
>      }
> +
> +    free(ofprotos);
>  }
>  
>  static void
> @@ -7122,7 +7180,7 @@ ofproto_unixctl_init(void)
>                               ofproto_unixctl_fdb_delete, NULL);
>      unixctl_command_register("fdb/flush", "[bridge]", 0, 1,
>                               ofproto_unixctl_fdb_flush, NULL);
> -    unixctl_command_register("fdb/show", "bridge", 1, 1,
> +    unixctl_command_register("fdb/show", "[bridge...]", 0, INT_MAX,
>                               ofproto_unixctl_fdb_show, NULL);
>      unixctl_command_register("fdb/stats-clear", "[bridge]", 0, 1,
>                               ofproto_unixctl_fdb_stats_clear, NULL);
> diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
> index 9e64e8ee4bd7..56e3b8a65f97 100644
> --- a/tests/ofproto-dpif.at
> +++ b/tests/ofproto-dpif.at
> @@ -7543,6 +7543,12 @@ add_of_ports br0 1 2 3
>  
>  
> arp='eth_type(0x0806),arp(sip=192.168.0.1,tip=192.168.0.2,op=1,sha=50:54:00:00:00:05,tha=00:00:00:00:00:00)'
>  
> +dnl Check json output for an empty FDB.
> +AT_CHECK([ovs-appctl --format json --pretty fdb/show br0], [0], [dnl
> +[{
> +  "br0": []}]
> +])
> +
>  # Trace an ARP packet arriving on p3, to create a MAC learning entry.
>  OFPROTO_TRACE(
>    [ovs-dummy],
> @@ -7574,17 +7580,18 @@ AT_CHECK_UNQUOTED([ovs-appctl fdb/show br0 | sed 
> 's/[[0-9]]\{1,\}$/?/'], [0], [d
>  dnl Check json output.
>  AT_CHECK([ovs-appctl --format json --pretty fdb/show br0 \
>            | sed 's/"age": [[0-9]]*/"age": ?/g'], [0], [dnl
> -[[
> -  {
> -    "age": ?,
> -    "mac": "50:54:00:00:00:05",
> -    "port": 3,
> -    "vlan": 0},
> -  {
> -    "age": ?,
> -    "mac": "50:54:00:00:00:06",
> -    "port": 1,
> -    "vlan": 0}]]
> +[{
> +  "br0": [
> +    {
> +      "age": ?,
> +      "mac": "50:54:00:00:00:05",
> +      "port": 3,
> +      "vlan": 0},
> +    {
> +      "age": ?,
> +      "mac": "50:54:00:00:00:06",
> +      "port": 1,
> +      "vlan": 0}]}]
>  ])
>  
>  # Trace a packet arrival that updates the first learned MAC entry.
> @@ -7639,6 +7646,96 @@ AT_CHECK_UNQUOTED([ovs-appctl fdb/show br1 | sed 
> 's/[[0-9]]\{1,\}$/?/'], [0], [d
>      5     0  50:54:00:00:00:07    ?
>  ])
>  
> +# Test fdb/show with multiple bridges.  A "Bridge" column groups each
> +# bridge's entries, printing the name only on the bridge's first row.
> +AT_CHECK_UNQUOTED([ovs-appctl fdb/show br0 br1 | sed 's/[[0-9]]\{1,\}$/?/'], 
> [0], [dnl
> + Bridge port  VLAN  MAC                Age
> + br0       2     0  50:54:00:00:00:05    ?
> + br1       5     0  50:54:00:00:00:07    ?
> +])
> +
> +# Test fdb/show without arguments: show all bridges.
> +AT_CHECK_UNQUOTED([ovs-appctl fdb/show | sed 's/[[0-9]]\{1,\}$/?/' | sort], 
> [0], [dnl
> + Bridge port  VLAN  MAC                Age
> + br0       2     0  50:54:00:00:00:05    ?
> + br1       5     0  50:54:00:00:00:07    ?
> +])
> +
> +dnl Check json output with multiple bridges: keyed by bridge name.
> +AT_CHECK([ovs-appctl --format json --pretty fdb/show br0 br1 \
> +          | sed 's/"age": [[0-9]]*/"age": ?/g'], [0], [dnl
> +[{
> +  "br0": [
> +    {
> +      "age": ?,
> +      "mac": "50:54:00:00:00:05",
> +      "port": 2,
> +      "vlan": 0}],
> +  "br1": [
> +    {
> +      "age": ?,
> +      "mac": "50:54:00:00:00:07",
> +      "port": 5,
> +      "vlan": 0}]}]
> +])
> +
> +# Test fdb/show with a non-existent bridge mixed in: must reply with an
> +# error and emit no partial output.
> +AT_CHECK([ovs-appctl fdb/show br0 nonexistent], [2], [],
> +  [no such bridge
> +ovs-appctl: ovs-vswitchd: server returned an error
> +])
> +
> +OVS_VSWITCHD_STOP
> +AT_CLEANUP
> +
> +AT_SETUP([ofproto-dpif - fdb/show bridge column width])
> +OVS_VSWITCHD_START
> +AT_CHECK([ovs-vsctl -- add-br br-nexthop -- set bridge br-nexthop 
> datapath-type=dummy])
> +add_of_ports br0 1 3
> +add_of_ports br-nexthop 2
> +
> +dnl br0 holds several MAC entries; br-nexthop holds one.
> +AT_CHECK([ovs-appctl fdb/add br0 p1 0 50:54:00:00:00:05])
> +AT_CHECK([ovs-appctl fdb/add br0 p3 0 50:54:00:00:00:06])
> +AT_CHECK([ovs-appctl fdb/add br-nexthop p2 0 50:54:00:00:00:07])
> +
> +dnl The "Bridge" column widens to the longest bridge name shown.  A
> +dnl bridge with several MAC entries prints its name only on the first
> +dnl row; the remaining rows leave the column blank so the entries read
> +dnl as one group.
> +AT_CHECK([ovs-appctl fdb/show br0 br-nexthop], [0], [dnl
> + Bridge     port  VLAN  MAC                Age
> + br0           1     0  50:54:00:00:00:05  static
> +               3     0  50:54:00:00:00:06  static
> + br-nexthop    2     0  50:54:00:00:00:07  static
> +])
> +
> +dnl A bridge with no entries still shows a name-only row so it stays
> +dnl visible in a multi-bridge listing (matching JSON, which lists []).
> +AT_CHECK([ovs-vsctl -- add-br br-empty -- set bridge br-empty 
> datapath-type=dummy])
> +AT_CHECK([ovs-appctl fdb/show br0 br-empty], [0], [dnl
> + Bridge   port  VLAN  MAC                Age
> + br0         1     0  50:54:00:00:00:05  static
> +             3     0  50:54:00:00:00:06  static
> + br-empty
> +])
> +AT_CHECK([ovs-appctl --format json --pretty fdb/show br0 br-empty], [0], [dnl
> +[{
> +  "br-empty": [],
> +  "br0": [
> +    {
> +      "mac": "50:54:00:00:00:05",
> +      "port": 1,
> +      "static": true,
> +      "vlan": 0},
> +    {
> +      "mac": "50:54:00:00:00:06",
> +      "port": 3,
> +      "static": true,
> +      "vlan": 0}]}]
> +])
> +
>  OVS_VSWITCHD_STOP
>  AT_CLEANUP
>  
> @@ -8197,27 +8294,28 @@ AT_CHECK_UNQUOTED([ovs-appctl fdb/show br0 | sed 's/ 
> *[[0-9]]\{1,\}$//' | grep -
>  dnl Check json output.
>  AT_CHECK([ovs-appctl --format json --pretty fdb/show br0 \
>            | sed 's/"age": [[0-9]]*/"age": ?/g'], [0], [dnl
> -[[
> -  {
> -    "age": ?,
> -    "mac": "50:54:00:00:00:01",
> -    "port": 1,
> -    "vlan": 0},
> -  {
> -    "age": ?,
> -    "mac": "50:54:00:00:00:02",
> -    "port": 2,
> -    "vlan": 0},
> -  {
> -    "mac": "50:54:00:00:01:01",
> -    "port": 1,
> -    "static": true,
> -    "vlan": 0},
> -  {
> -    "mac": "50:54:00:00:02:02",
> -    "port": 2,
> -    "static": true,
> -    "vlan": 0}]]
> +[{
> +  "br0": [
> +    {
> +      "age": ?,
> +      "mac": "50:54:00:00:00:01",
> +      "port": 1,
> +      "vlan": 0},
> +    {
> +      "age": ?,
> +      "mac": "50:54:00:00:00:02",
> +      "port": 2,
> +      "vlan": 0},
> +    {
> +      "mac": "50:54:00:00:01:01",
> +      "port": 1,
> +      "static": true,
> +      "vlan": 0},
> +    {
> +      "mac": "50:54:00:00:02:02",
> +      "port": 2,
> +      "static": true,
> +      "vlan": 0}]}]
>  ])
>  
>  dnl Remove static mac entry.
> diff --git a/vswitchd/ovs-vswitchd.8.in b/vswitchd/ovs-vswitchd.8.in
> index b21b1f08e893..3d80bceb655b 100644
> --- a/vswitchd/ovs-vswitchd.8.in
> +++ b/vswitchd/ovs-vswitchd.8.in
> @@ -176,10 +176,16 @@ Deletes \fImac\fR address from a \fIport\fR and 
> \fIvlan\fR on a \fIbridge\fR.
>  .IP "\fBfdb/flush\fR [\fIbridge\fR]"
>  Flushes \fIbridge\fR MAC address learning table, or all learning tables
>  if no \fIbridge\fR is given.
> -.IP "\fBfdb/show\fR \fIbridge\fR"
> +.IP "\fBfdb/show\fR [\fIbridge\fR...]"
>  Lists each MAC address/VLAN pair learned by the specified \fIbridge\fR,
>  along with the port on which it was learned and the age of the entry,
> -in seconds.
> +in seconds.  Multiple bridge names may be given to query several bridges
> +in a single call, and if no bridge is given the tables of all bridges are
> +shown.  When more than one bridge is shown, text output gains a leading
> +"Bridge" column that groups each bridge's entries, printing the bridge
> +name only on its first row; a bridge with no entries is listed as a
> +name-only row.  JSON output is an object keyed by bridge name, whose
> +values are arrays of that bridge's entries.
>  .IP "\fBfdb/stats-clear\fR [\fIbridge\fR]"
>  Clear \fIbridge\fR MAC address learning table statistics, or all
>  statistics if no \fIbridge\fR is given.

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to