]> git.puffer.fish Git - matthieu/frr.git/commitdiff
bgpd: add show bgp summary filter by neighbor or AS
authorLouis Scalbert <louis.scalbert@6wind.com>
Wed, 12 May 2021 15:17:56 +0000 (17:17 +0200)
committerLouis Scalbert <louis.scalbert@6wind.com>
Wed, 26 May 2021 14:16:08 +0000 (16:16 +0200)
Add ability to filter session on show bgp summary by neighbor or
remote AS:

ubuntu# show bgp summary ?
  neighbor     Show only the specified neighbor session
  remote-as    Show only the specified remote AS session
ubuntu# show bgp summary neighbor ?
  A.B.C.D   Neighbor to display information about
  WORD      Neighbor on BGP configured interface
  X:X::X:X  Neighbor to display information about
ubuntu# show bgp summary remote-as ?
  (1-4294967295)  AS number
  external        External (eBGP) AS sessions
  internal        Internal (iBGP) AS sessions

This patch includes the documentation and the topotest.

Signed-off-by: Louis Scalbert <louis.scalbert@6wind.com>
bgpd/bgp_evpn_vty.c
bgpd/bgp_vty.c
bgpd/bgp_vty.h
doc/user/bgp.rst
tests/topotests/all_protocol_startup/test_all_protocol_startup.py

index ed8a6a9506001b3c15527ad20beb72cb3632da62..fb3ba2c0ec85c1089f66dc58c867dc56907b647f 100644 (file)
@@ -4309,24 +4309,30 @@ DEFPY(show_bgp_l2vpn_evpn_nh,
 /*
  * Display EVPN neighbor summary.
  */
-DEFUN(show_bgp_l2vpn_evpn_summary,
-      show_bgp_l2vpn_evpn_summary_cmd,
-      "show bgp [vrf VRFNAME] l2vpn evpn summary [established|failed] [wide] [json]",
-      SHOW_STR
-      BGP_STR
+DEFUN(show_bgp_l2vpn_evpn_summary, show_bgp_l2vpn_evpn_summary_cmd,
+      "show bgp [vrf VRFNAME] l2vpn evpn summary [established|failed] [<neighbor <A.B.C.D|X:X::X:X|WORD>|remote-as <(1-4294967295)|internal|external>>] [wide] [json]",
+      SHOW_STR BGP_STR
       "bgp vrf\n"
-      "vrf name\n"
-      L2VPN_HELP_STR
-      EVPN_HELP_STR
+      "vrf name\n" L2VPN_HELP_STR EVPN_HELP_STR
       "Summary of BGP neighbor status\n"
       "Show only sessions in Established state\n"
       "Show only sessions not in Established state\n"
-      "Increase table width for longer output\n"
-      JSON_STR)
+      "Show only the specified neighbor session\n"
+      "Neighbor to display information about\n"
+      "Neighbor to display information about\n"
+      "Neighbor on BGP configured interface\n"
+      "Show only the specified remote AS sessions\n"
+      "AS number\n"
+      "Internal (iBGP) AS sessions\n"
+      "External (eBGP) AS sessions\n"
+      "Increase table width for longer output\n" JSON_STR)
 {
        int idx_vrf = 0;
        int idx = 0;
        char *vrf = NULL;
+       char *neighbor = NULL;
+       as_t as = 0; /* 0 means AS filter not set */
+       int as_type = AS_UNSPECIFIED;
        uint8_t show_flags = 0;
 
        if (argv_find(argv, argc, "vrf", &idx_vrf))
@@ -4338,13 +4344,27 @@ DEFUN(show_bgp_l2vpn_evpn_summary,
        if (argv_find(argv, argc, "established", &idx))
                SET_FLAG(show_flags, BGP_SHOW_OPT_ESTABLISHED);
 
+
+       if (argv_find(argv, argc, "neighbor", &idx))
+               neighbor = argv[idx + 1]->arg;
+
+       if (argv_find(argv, argc, "remote-as", &idx)) {
+               if (argv[idx + 1]->arg[0] == 'i')
+                       as_type = AS_INTERNAL;
+               else if (argv[idx + 1]->arg[0] == 'e')
+                       as_type = AS_EXTERNAL;
+               else
+                       as = (as_t)atoi(argv[idx + 1]->arg);
+       }
+
        if (argv_find(argv, argc, "wide", &idx))
                SET_FLAG(show_flags, BGP_SHOW_OPT_WIDE);
 
        if (use_json(argc, argv))
                SET_FLAG(show_flags, BGP_SHOW_OPT_JSON);
 
-       return bgp_show_summary_vty(vty, vrf, AFI_L2VPN, SAFI_EVPN, show_flags);
+       return bgp_show_summary_vty(vty, vrf, AFI_L2VPN, SAFI_EVPN, neighbor,
+                                   as_type, as, show_flags);
 }
 
 int bgp_evpn_cli_parse_type(int *type, struct cmd_token **argv, int argc)
index 77f1aadff701452d729973f6a83aa084437a378e..b2769e21d911e69836aab12021ea61badafe927f 100644 (file)
@@ -10685,8 +10685,35 @@ static char *bgp_peer_description_stripped(char *desc, uint32_t size)
        return stripped;
 }
 
+/* Determine whether var peer should be filtered out of the summary. */
+static bool bgp_show_summary_is_peer_filtered(struct peer *peer,
+                                             struct peer *fpeer, int as_type,
+                                             as_t as)
+{
+
+       /* filter neighbor XXXX */
+       if (fpeer && fpeer != peer)
+               return true;
+
+       /* filter remote-as (internal|external) */
+       if (as_type != AS_UNSPECIFIED) {
+               if (peer->as_type == AS_SPECIFIED) {
+                       if (as_type == AS_INTERNAL) {
+                               if (peer->as != peer->local_as)
+                                       return true;
+                       } else if (peer->as == peer->local_as)
+                               return true;
+               } else if (as_type != peer->as_type)
+                       return true;
+       } else if (as && as != peer->as) /* filter remote-as XXX */
+               return true;
+
+       return false;
+}
+
 /* Show BGP peer's summary information. */
 static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
+                           struct peer *fpeer, int as_type, as_t as,
                            uint8_t show_flags)
 {
        struct peer *peer;
@@ -10723,6 +10750,12 @@ static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
                json = json_object_new_object();
                json_peers = json_object_new_object();
                for (ALL_LIST_ELEMENTS(bgp->peer, node, nnode, peer)) {
+                       if (bgp_show_summary_is_peer_filtered(peer, fpeer,
+                                                             as_type, as)) {
+                               count++;
+                               continue;
+                       }
+
                        if (!CHECK_FLAG(peer->flags, PEER_FLAG_CONFIG_NODE))
                                continue;
 
@@ -10742,6 +10775,12 @@ static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
                 * characters are needed for the Neighbor column
                 */
                for (ALL_LIST_ELEMENTS(bgp->peer, node, nnode, peer)) {
+                       if (bgp_show_summary_is_peer_filtered(peer, fpeer,
+                                                             as_type, as)) {
+                               count++;
+                               continue;
+                       }
+
                        if (!CHECK_FLAG(peer->flags, PEER_FLAG_CONFIG_NODE))
                                continue;
 
@@ -11025,6 +11064,10 @@ static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
                if (use_json) {
                        json_peer = NULL;
 
+                       if (bgp_show_summary_is_peer_filtered(peer, fpeer,
+                                                             as_type, as))
+                               continue;
+
                        if (show_failed &&
                            bgp_has_peer_failed(peer, afi, safi)) {
                                json_peer = json_object_new_object();
@@ -11174,6 +11217,9 @@ static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
                        json_object_object_add(json_peers, peer->host,
                                               json_peer);
                } else {
+                       if (bgp_show_summary_is_peer_filtered(peer, fpeer,
+                                                             as_type, as))
+                               continue;
                        if (show_failed &&
                            bgp_has_peer_failed(peer, afi, safi)) {
                                bgp_show_failed_summary(vty, bgp, peer, NULL,
@@ -11183,7 +11229,6 @@ static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
                                if (show_established
                                    && bgp_has_peer_failed(peer, afi, safi))
                                        continue;
-
                                memset(dn_flag, '\0', sizeof(dn_flag));
                                if (peer_dynamic_neighbor(peer)) {
                                        dn_flag[0] = '*';
@@ -11336,7 +11381,8 @@ static int bgp_show_summary(struct vty *vty, struct bgp *bgp, int afi, int safi,
 }
 
 static void bgp_show_summary_afi_safi(struct vty *vty, struct bgp *bgp, int afi,
-                                     int safi, uint8_t show_flags)
+                                     int safi, struct peer *fpeer, int as_type,
+                                     as_t as, uint8_t show_flags)
 {
        int is_first = 1;
        int afi_wildcard = (afi == AFI_MAX);
@@ -11380,8 +11426,8 @@ static void bgp_show_summary_afi_safi(struct vty *vty, struct bgp *bgp, int afi,
                                                                         false));
                                        }
                                }
-                               bgp_show_summary(vty, bgp, afi, safi,
-                                                show_flags);
+                               bgp_show_summary(vty, bgp, afi, safi, fpeer,
+                                                as_type, as, show_flags);
                        }
                        safi++;
                        if (!safi_wildcard)
@@ -11403,10 +11449,14 @@ static void bgp_show_summary_afi_safi(struct vty *vty, struct bgp *bgp, int afi,
 }
 
 static void bgp_show_all_instances_summary_vty(struct vty *vty, afi_t afi,
-                                              safi_t safi, uint8_t show_flags)
+                                              safi_t safi,
+                                              const char *neighbor,
+                                              int as_type, as_t as,
+                                              uint8_t show_flags)
 {
        struct listnode *node, *nnode;
        struct bgp *bgp;
+       struct peer *fpeer = NULL;
        int is_first = 1;
        bool nbr_output = false;
        bool use_json = CHECK_FLAG(show_flags, BGP_SHOW_OPT_JSON);
@@ -11432,7 +11482,14 @@ static void bgp_show_all_instances_summary_vty(struct vty *vty, afi_t afi,
                                        ? VRF_DEFAULT_NAME
                                        : bgp->name);
                }
-               bgp_show_summary_afi_safi(vty, bgp, afi, safi, show_flags);
+               if (neighbor) {
+                       fpeer = peer_lookup_in_view(vty, bgp, neighbor,
+                                                   use_json);
+                       if (!fpeer)
+                               continue;
+               }
+               bgp_show_summary_afi_safi(vty, bgp, afi, safi, fpeer, as_type,
+                                         as, show_flags);
        }
 
        if (use_json)
@@ -11442,15 +11499,18 @@ static void bgp_show_all_instances_summary_vty(struct vty *vty, afi_t afi,
 }
 
 int bgp_show_summary_vty(struct vty *vty, const char *name, afi_t afi,
-                        safi_t safi, uint8_t show_flags)
+                        safi_t safi, const char *neighbor, int as_type,
+                        as_t as, uint8_t show_flags)
 {
        struct bgp *bgp;
        bool use_json = CHECK_FLAG(show_flags, BGP_SHOW_OPT_JSON);
+       struct peer *fpeer = NULL;
 
        if (name) {
                if (strmatch(name, "all")) {
                        bgp_show_all_instances_summary_vty(vty, afi, safi,
-                                                          show_flags);
+                                                          neighbor, as_type,
+                                                          as, show_flags);
                        return CMD_SUCCESS;
                } else {
                        bgp = bgp_lookup_by_name(name);
@@ -11464,17 +11524,30 @@ int bgp_show_summary_vty(struct vty *vty, const char *name, afi_t afi,
                                return CMD_WARNING;
                        }
 
-                       bgp_show_summary_afi_safi(vty, bgp, afi, safi,
-                                                 show_flags);
+                       if (neighbor) {
+                               fpeer = peer_lookup_in_view(vty, bgp, neighbor,
+                                                           use_json);
+                               if (!fpeer)
+                                       return CMD_WARNING;
+                       }
+                       bgp_show_summary_afi_safi(vty, bgp, afi, safi, fpeer,
+                                                 as_type, as, show_flags);
                        return CMD_SUCCESS;
                }
        }
 
        bgp = bgp_get_default();
 
-       if (bgp)
-               bgp_show_summary_afi_safi(vty, bgp, afi, safi, show_flags);
-       else {
+       if (bgp) {
+               if (neighbor) {
+                       fpeer = peer_lookup_in_view(vty, bgp, neighbor,
+                                                   use_json);
+                       if (!fpeer)
+                               return CMD_WARNING;
+               }
+               bgp_show_summary_afi_safi(vty, bgp, afi, safi, fpeer, as_type,
+                                         as, show_flags);
+       } else {
                if (use_json)
                        vty_out(vty, "{}\n");
                else
@@ -11486,25 +11559,31 @@ int bgp_show_summary_vty(struct vty *vty, const char *name, afi_t afi,
 }
 
 /* `show [ip] bgp summary' commands. */
-DEFPY (show_ip_bgp_summary,
-       show_ip_bgp_summary_cmd,
-       "show [ip] bgp [<view|vrf> VIEWVRFNAME] ["BGP_AFI_CMD_STR" ["BGP_SAFI_WITH_LABEL_CMD_STR"]] [all$all] summary [established|failed] [wide] [json$uj]",
-       SHOW_STR
-       IP_STR
-       BGP_STR
-       BGP_INSTANCE_HELP_STR
-       BGP_AFI_HELP_STR
-       BGP_SAFI_WITH_LABEL_HELP_STR
-       "Display the entries for all address families\n"
-       "Summary of BGP neighbor status\n"
-       "Show only sessions in Established state\n"
-       "Show only sessions not in Established state\n"
-       "Increase table width for longer output\n"
-       JSON_STR)
+DEFPY(show_ip_bgp_summary, show_ip_bgp_summary_cmd,
+      "show [ip] bgp [<view|vrf> VIEWVRFNAME] [" BGP_AFI_CMD_STR
+      " [" BGP_SAFI_WITH_LABEL_CMD_STR
+      "]] [all$all] summary [established|failed] [<neighbor <A.B.C.D|X:X::X:X|WORD>|remote-as <(1-4294967295)|internal|external>>] [wide] [json$uj]",
+      SHOW_STR IP_STR BGP_STR BGP_INSTANCE_HELP_STR BGP_AFI_HELP_STR
+             BGP_SAFI_WITH_LABEL_HELP_STR
+      "Display the entries for all address families\n"
+      "Summary of BGP neighbor status\n"
+      "Show only sessions in Established state\n"
+      "Show only sessions not in Established state\n"
+      "Show only the specified neighbor session\n"
+      "Neighbor to display information about\n"
+      "Neighbor to display information about\n"
+      "Neighbor on BGP configured interface\n"
+      "Show only the specified remote AS sessions\n"
+      "AS number\n"
+      "Internal (iBGP) AS sessions\n"
+      "External (eBGP) AS sessions\n"
+      "Increase table width for longer output\n" JSON_STR)
 {
        char *vrf = NULL;
        afi_t afi = AFI_MAX;
        safi_t safi = SAFI_MAX;
+       as_t as = 0; /* 0 means AS filter not set */
+       int as_type = AS_UNSPECIFIED;
        uint8_t show_flags = 0;
 
        int idx = 0;
@@ -11531,13 +11610,23 @@ DEFPY (show_ip_bgp_summary,
        if (argv_find(argv, argc, "established", &idx))
                SET_FLAG(show_flags, BGP_SHOW_OPT_ESTABLISHED);
 
+       if (argv_find(argv, argc, "remote-as", &idx)) {
+               if (argv[idx + 1]->arg[0] == 'i')
+                       as_type = AS_INTERNAL;
+               else if (argv[idx + 1]->arg[0] == 'e')
+                       as_type = AS_EXTERNAL;
+               else
+                       as = (as_t)atoi(argv[idx + 1]->arg);
+       }
+
        if (argv_find(argv, argc, "wide", &idx))
                SET_FLAG(show_flags, BGP_SHOW_OPT_WIDE);
 
        if (argv_find(argv, argc, "json", &idx))
                SET_FLAG(show_flags, BGP_SHOW_OPT_JSON);
 
-       return bgp_show_summary_vty(vty, vrf, afi, safi, show_flags);
+       return bgp_show_summary_vty(vty, vrf, afi, safi, neighbor, as_type, as,
+                                   show_flags);
 }
 
 const char *get_afi_safi_str(afi_t afi, safi_t safi, bool for_json)
index 051b2e958010d9063fb9fd9621dacfff627d11ab..2531488d0de22472ecba47d110307e5a6a4e573f 100644 (file)
@@ -185,7 +185,8 @@ extern int bgp_vty_find_and_parse_afi_safi_bgp(struct vty *vty,
 int bgp_vty_find_and_parse_bgp(struct vty *vty, struct cmd_token **argv,
                               int argc, struct bgp **bgp, bool use_json);
 extern int bgp_show_summary_vty(struct vty *vty, const char *name, afi_t afi,
-                               safi_t safi, uint8_t show_flags);
+                               safi_t safi, const char *neighbor, int as_type,
+                               as_t as, uint8_t show_flags);
 extern int bgp_clear_star_soft_in(const char *name, char *errmsg,
                                  size_t errmsg_len);
 extern int bgp_clear_star_soft_out(const char *name, char *errmsg,
index 7f653cb7b715a29cc7b8fa0e57dc504ad51d17ad..dd042e2584412f39ba4ecb88692c6afbadea8ac1 100644 (file)
@@ -3284,6 +3284,19 @@ structure is extended with :clicmd:`show bgp [afi] [safi]`.
    Show a bgp peer summary for peers that are succesfully exchanging routes
    for the specified address family, and subsequent address-family.
 
+.. clicmd:: show bgp [afi] [safi] [all] summary neighbor [PEER] [json]
+
+   Show a bgp summary for the specified peer, address family, and
+   subsequent address-family. The neighbor filter can be used in combination
+   with the failed, established filters.
+
+.. clicmd:: show bgp [afi] [safi] [all] summary remote-as <internal|external|ASN> [json]
+
+   Show a bgp peer summary for the specified remote-as ASN or type (``internal``
+   for iBGP and ``external`` for eBGP sessions), address family, and subsequent
+   address-family. The remote-as filter can be used in combination with the
+   failed, established filters.
+
 .. clicmd:: show bgp [afi] [safi] [neighbor [PEER] [routes|advertised-routes|received-routes] [json]
 
    This command shows information on a specific BGP peer of the relevant
index c10e32ad0a552fde07879e60ee5d185d3ac7c0b8..597e2306965ac7e0a41eeb0acc118dd0db7abfc4 100644 (file)
@@ -904,74 +904,128 @@ def test_bgp_summary():
         refTableFile = "%s/r%s/show_ip_bgp_summary.ref" % (thisDir, i)
         if os.path.isfile(refTableFile):
             # Read expected result from file
-            expected = open(refTableFile).read().rstrip()
-            # Fix newlines (make them all the same)
-            expected = ("\n".join(expected.splitlines()) + "\n").splitlines(1)
+            expected_original = open(refTableFile).read().rstrip()
 
-            # Actual output from router
-            actual = (
-                net["r%s" % i]
-                .cmd('vtysh -c "show ip bgp summary" 2> /dev/null')
-                .rstrip()
-            )
-            # Mask out "using XXiXX bytes" portion. They are random...
-            actual = re.sub(r"using [0-9]+ bytes", "using XXXX bytes", actual)
-            # Mask out "using XiXXX KiB" portion. They are random...
-            actual = re.sub(r"using [0-9]+ KiB", "using XXXX KiB", actual)
-            #
-            # Remove extra summaries which exist with newer versions
-            #
-            # Remove summary lines (changed recently)
-            actual = re.sub(r"Total number.*", "", actual)
-            actual = re.sub(r"Displayed.*", "", actual)
-            # Remove IPv4 Unicast Summary (Title only)
-            actual = re.sub(r"IPv4 Unicast Summary:", "", actual)
-            # Remove IPv4 Multicast Summary (all of it)
-            actual = re.sub(r"IPv4 Multicast Summary:", "", actual)
-            actual = re.sub(r"No IPv4 Multicast neighbor is configured", "", actual)
-            # Remove IPv4 VPN Summary (all of it)
-            actual = re.sub(r"IPv4 VPN Summary:", "", actual)
-            actual = re.sub(r"No IPv4 VPN neighbor is configured", "", actual)
-            # Remove IPv4 Encap Summary (all of it)
-            actual = re.sub(r"IPv4 Encap Summary:", "", actual)
-            actual = re.sub(r"No IPv4 Encap neighbor is configured", "", actual)
-            # Remove Unknown Summary (all of it)
-            actual = re.sub(r"Unknown Summary:", "", actual)
-            actual = re.sub(r"No Unknown neighbor is configured", "", actual)
+            for filter in ["", "remote-as internal", "remote-as external",
+                           "remote-as 100", "remote-as 123",
+                           "neighbor 192.168.7.10", "neighbor 192.168.7.10",
+                           "neighbor fc00:0:0:8::1000",
+                           "neighbor 10.0.0.1"]:
+                # Actual output from router
+                actual = (
+                    net["r%s" % i]
+                    .cmd('vtysh -c "show ip bgp summary ' + filter + '" 2> /dev/null')
+                    .rstrip()
+                )
+                # Mask out "using XXiXX bytes" portion. They are random...
+                actual = re.sub(r"using [0-9]+ bytes", "using XXXX bytes", actual)
+                # Mask out "using XiXXX KiB" portion. They are random...
+                actual = re.sub(r"using [0-9]+ KiB", "using XXXX KiB", actual)
 
-            actual = re.sub(r"IPv4 labeled-unicast Summary:", "", actual)
-            actual = re.sub(
-                r"No IPv4 labeled-unicast neighbor is configured", "", actual
-            )
+                # Remove extra summaries which exist with newer versions
 
-            # Strip empty lines
-            actual = actual.lstrip()
-            actual = actual.rstrip()
-            #
-            # Fix newlines (make them all the same)
-            actual = ("\n".join(actual.splitlines()) + "\n").splitlines(1)
+                # Remove summary lines (changed recently)
+                actual = re.sub(r"Total number.*", "", actual)
+                actual = re.sub(r"Displayed.*", "", actual)
+                # Remove IPv4 Unicast Summary (Title only)
+                actual = re.sub(r"IPv4 Unicast Summary:", "", actual)
+                # Remove IPv4 Multicast Summary (all of it)
+                actual = re.sub(r"IPv4 Multicast Summary:", "", actual)
+                actual = re.sub(r"No IPv4 Multicast neighbor is configured", "", actual)
+                # Remove IPv4 VPN Summary (all of it)
+                actual = re.sub(r"IPv4 VPN Summary:", "", actual)
+                actual = re.sub(r"No IPv4 VPN neighbor is configured", "", actual)
+                # Remove IPv4 Encap Summary (all of it)
+                actual = re.sub(r"IPv4 Encap Summary:", "", actual)
+                actual = re.sub(r"No IPv4 Encap neighbor is configured", "", actual)
+                # Remove Unknown Summary (all of it)
+                actual = re.sub(r"Unknown Summary:", "", actual)
+                actual = re.sub(r"No Unknown neighbor is configured", "", actual)
+
+                actual = re.sub(r"IPv4 labeled-unicast Summary:", "", actual)
+                actual = re.sub(
+                    r"No IPv4 labeled-unicast neighbor is configured", "", actual
+                )
 
-            # Generate Diff
-            diff = topotest.get_textdiff(
-                actual,
-                expected,
-                title1="actual SHOW IP BGP SUMMARY",
-                title2="expected SHOW IP BGP SUMMARY",
-            )
+                expected = expected_original
+                # apply filters on expected output
+                if "internal" in filter or "remote-as 100" in filter:
+                    expected = re.sub(r".+\s+200\s+.+", "", expected)
+                elif "external" in filter:
+                    expected = re.sub(r".+\s+100\s+.+Active.+", "", expected)
+                elif "remote-as 123" in filter:
+                    expected = re.sub(
+                        r"(192.168.7.(1|2)0|fc00:0:0:8::(1|2)000).+Active.+",
+                        "", expected
+                    )
+                elif "192.168.7.10" in filter:
+                    expected = re.sub(
+                        r"(192.168.7.20|fc00:0:0:8::(1|2)000).+Active.+",
+                        "", expected
+                    )
+                elif "fc00:0:0:8::1000" in filter:
+                    expected = re.sub(
+                        r"(192.168.7.(1|2)0|fc00:0:0:8::2000).+Active.+",
+                        "", expected
+                    )
+                elif "10.0.0.1" in filter:
+                    expected = "No such neighbor in this view/vrf"
+
+                # Strip empty lines
+                actual = actual.lstrip().rstrip()
+                expected = expected.lstrip().rstrip()
+                actual = re.sub(r"\n+", "\n", actual)
+                expected = re.sub(r"\n+", "\n", expected)
+
+                # reapply initial formatting
+                actual = re.sub(r"KiB of memory\n", "KiB of memory\n\n", actual)
+                expected = re.sub(r"KiB of memory\n", "KiB of memory\n\n", expected)
+
+                # realign expected neighbor columns if needed
+                try:
+                    idx_actual = re.search(r"\n(Neighbor\s+V\s+)", actual).group(1).find("V")
+                    idx_expected = re.search(r"\n(Neighbor\s+V\s+)", expected).group(1).find("V")
+                    idx_diff = idx_expected - idx_actual
+                    if idx_diff > 0:
+                        # Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd
+                        expected = re.sub(" " * idx_diff + "V ", "V ", expected)
+                        # 192.168.7.10    4        100       0       0        0    0    0    never       Active
+                        expected = re.sub(" " * idx_diff + "4 ", "4 ", expected)
+                except AttributeError:
+                    pass
 
-            # Empty string if it matches, otherwise diff contains unified diff
-            if diff:
-                sys.stderr.write(
-                    "r%s failed SHOW IP BGP SUMMARY check:\n%s\n" % (i, diff)
+                # Fix newlines (make them all the same)
+                actual = ("\n".join(actual.splitlines()) + "\n").splitlines(1)
+                expected = ("\n".join(expected.splitlines()) + "\n").splitlines(1)
+
+                # Generate Diff
+                diff = topotest.get_textdiff(
+                    actual,
+                    expected,
+                    title1="actual SHOW IP BGP SUMMARY " + filter.upper() ,
+                    title2="expected SHOW IP BGP SUMMARY " + filter.upper(),
                 )
-                failures += 1
-            else:
-                print("r%s ok" % i)
 
-            assert failures == 0, "SHOW IP BGP SUMMARY failed for router r%s:\n%s" % (
-                i,
-                diff,
-            )
+                # Empty string if it matches, otherwise diff contains unified diff
+                if diff:
+                    sys.stderr.write(
+                        "r%s failed SHOW IP BGP SUMMARY check:\n%s\n" % (i, diff)
+                    )
+                    failures += 1
+                else:
+                    print("r%s ok" % i)
+
+                assert failures == 0, "SHOW IP BGP SUMMARY failed for router r%s:\n%s" % (
+                    i,
+                    diff,
+                )
+
+                # Actual output from router
+                actual = (
+                    net["r%s" % i]
+                    .cmd('vtysh -c "show ip bgp summary" 2> /dev/null')
+                    .rstrip()
+                )
 
     # Make sure that all daemons are running
     for i in range(1, 2):