docs: def get_bgp_neighbors(self):
          """BGP neighbor information.
  
          Currently no VRF support. Supports both IPv4 and IPv6.
          """
          supported_afi = ["ipv4", "ipv6"]
  
          bgp_neighbor_data = dict()
          bgp_neighbor_data["global"] = {}
  
          # get summary output from device
          cmd_bgp_all_sum = "show bgp all summary"
          summary_output = self._send_command(cmd_bgp_all_sum).strip()
  
          if "Invalid input detected" in summary_output:
              raise CommandErrorException("BGP is not running on this device")
  
          # get neighbor output from device
          neighbor_output = ""
          for afi in supported_afi:
              cmd_bgp_neighbor = "show bgp %s unicast neighbors" % afi
              neighbor_output += self._send_command(cmd_bgp_neighbor).strip()
              # trailing newline required for parsing
              neighbor_output += "\n"
  
          # Regular expressions used for parsing BGP summary
          parse_summary = {
              "patterns": [
                  # For address family: IPv4 Unicast
                  {
                      "regexp": re.compile(r"^For address family: (?P<afi>\S+) "),
                      "record": False,
                  },
                  # Capture router_id and local_as values, e.g.:
                  # BGP router identifier 10.0.1.1, local AS number 65000
                  {
                      "regexp": re.compile(
                          r"^.* router identifier (?P<router_id>{}), "
                          r"local AS number (?P<local_as>{})".format(
                              IPV4_ADDR_REGEX, ASN_REGEX
                          )
                      ),
                      "record": False,
                  },
                  # Match neighbor summary row, capturing useful details and
                  # discarding the 5 columns that we don't care about, e.g.:
                  # Neighbor   V          AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
                  # 10.0.0.2   4       65000 1336020 64337701 1011343614    0    0 8w0d         3143
                  {
                      "regexp": re.compile(
                          r"^\*?(?P<remote_addr>({})|({}))"
                          r"\s+\d+\s+(?P<remote_as>{})(\s+\S+){{5}}\s+"
                          r"(?P<uptime>(never)|\d+\S+)"
                          r"\s+(?P<accepted_prefixes>\d+)".format(
                              IPV4_ADDR_REGEX, IPV6_ADDR_REGEX, ASN_REGEX
                          )
                      ),
                      "record": True,
                  },
                  # Same as above, but for peer that are not Established, e.g.:
                  # Neighbor      V       AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
                  # 192.168.0.2   4    65002       0       0        1    0    0 never    Active
                  {
                      "regexp": re.compile(
                          r"^\*?(?P<remote_addr>({})|({}))"
                          r"\s+\d+\s+(?P<remote_as>{})(\s+\S+){{5}}\s+"
                          r"(?P<uptime>(never)|\d+\S+)\s+(?P<state>\D.*)".format(
                              IPV4_ADDR_REGEX, IPV6_ADDR_REGEX, ASN_REGEX
                          )
                      ),
                      "record": True,
                  },
                  # ipv6 peers often break accross rows because of the longer peer address,
                  # match as above, but in separate expressions, e.g.:
                  # Neighbor      V       AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
                  # 2001:DB8::4
                  #               4    65004 9900690  612449 155362939    0    0 26w6d       36391
                  {
                      "regexp": re.compile(
                          r"^\*?(?P<remote_addr>({})|({}))".format(
                              IPV4_ADDR_REGEX, IPV6_ADDR_REGEX
                          )
                      ),
                      "record": False,
                  },
                  {
                      "regexp": re.compile(
                          r"^\s+\d+\s+(?P<remote_as>{})(\s+\S+){{5}}\s+"
                          r"(?P<uptime>(never)|\d+\S+)"
                          r"\s+(?P<accepted_prefixes>\d+)".format(ASN_REGEX)
                      ),
                      "record": True,
                  },
                  # Same as above, but for peers that are not Established, e.g.:
                  # Neighbor      V       AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
                  # 2001:DB8::3
                  #               4    65003       0       0        1    0    0 never    Idle (Admin)
                  {
                      "regexp": re.compile(
                          r"^\s+\d+\s+(?P<remote_as>{})(\s+\S+){{5}}\s+"
                          r"(?P<uptime>(never)|\d+\S+)\s+(?P<state>\D.*)".format(
                              ASN_REGEX
                          )
                      ),
                      "record": True,
                  },
              ],
              "no_fill_fields": [
                  "accepted_prefixes",
                  "state",
                  "uptime",
                  "remote_as",
                  "remote_addr",
              ],
          }
  
          parse_neighbors = {
              "patterns": [
                  # Capture BGP neighbor is 10.0.0.2,  remote AS 65000, internal link
                  {
                      "regexp": re.compile(
                          r"^BGP neighbor is (?P<remote_addr>({})|({})),"
                          r"\s+remote AS (?P<remote_as>{}).*".format(
                              IPV4_ADDR_REGEX, IPV6_ADDR_REGEX, ASN_REGEX
                          )
                      ),
                      "record": False,
                  },
                  # Capture description
                  {
                      "regexp": re.compile(r"^\s+Description: (?P<description>.+)"),
                      "record": False,
                  },
                  # Capture remote_id, e.g.:
                  # BGP version 4, remote router ID 10.0.1.2
                  {
                      "regexp": re.compile(
                          r"^\s+BGP version \d+, remote router ID "
                          r"(?P<remote_id>{})".format(IPV4_ADDR_REGEX)
                      ),
                      "record": False,
                  },
                  # Capture state
                  {
                      "regexp": re.compile(r"^\s+BGP state = (?P<state>\w+)"),
                      "record": False,
                  },
                  # Capture AFI and SAFI names, e.g.:
                  # For address family: IPv4 Unicast
                  {
                      "regexp": re.compile(r"^\s+For address family: (?P<afi>\S+) "),
                      "record": False,
                  },
                  # Capture current sent and accepted prefixes, e.g.:
                  #     Prefixes Current:          637213       3142 (Consumes 377040 bytes)
                  {
                      "regexp": re.compile(
                          r"^\s+Prefixes Current:\s+(?P<sent_prefixes>\d+)\s+"
                          r"(?P<accepted_prefixes>\d+).*"
                      ),
                      "record": False,
                  },
                  # Capture received_prefixes if soft-reconfig is enabled for the peer
                  {
                      "regexp": re.compile(
                          r"^\s+Saved (soft-reconfig):.+(?P<received_prefixes>\d+).*"
                      ),
                      "record": True,
                  },
                  # Otherwise, use the following as an end of row marker
                  {
                      "regexp": re.compile(r"^\s+Local Policy Denied Prefixes:.+"),
                      "record": True,
                  },
              ],
              # fields that should not be "filled down" across table rows
              "no_fill_fields": [
                  "received_prefixes",
                  "accepted_prefixes",
                  "sent_prefixes",
              ],
          }
  
          # Parse outputs into a list of dicts
          summary_data = []
          summary_data_entry = {}
  
          for line in summary_output.splitlines():
              # check for matches against each pattern
              for item in parse_summary["patterns"]:
                  match = item["regexp"].match(line)
                  if match:
                      # a match was found, so update the temp entry with the match's groupdict
                      summary_data_entry.update(match.groupdict())
                      if item["record"]:
                          # Record indicates the last piece of data has been obtained; move
                          # on to next entry
                          summary_data.append(copy.deepcopy(summary_data_entry))
                          # remove keys that are listed in no_fill_fields before the next pass
                          for field in parse_summary["no_fill_fields"]:
                              try:
                                  del summary_data_entry[field]
                              except KeyError:
                                  pass
                      break
  
          neighbor_data = []
          neighbor_data_entry = {}
          for line in neighbor_output.splitlines():
              # check for matches against each pattern
              for item in parse_neighbors["patterns"]:
                  match = item["regexp"].match(line)
                  if match:
                      # a match was found, so update the temp entry with the match's groupdict
                      neighbor_data_entry.update(match.groupdict())
                      if item["record"]:
                          # Record indicates the last piece of data has been obtained; move
                          # on to next entry
                          neighbor_data.append(copy.deepcopy(neighbor_data_entry))
                          # remove keys that are listed in no_fill_fields before the next pass
                          for field in parse_neighbors["no_fill_fields"]:
                              try:
                                  del neighbor_data_entry[field]
                              except KeyError:
                                  pass
                      break
  
          router_id = None
  
          for entry in summary_data:
              if not router_id:
                  router_id = entry["router_id"]
              elif entry["router_id"] != router_id:
                  raise ValueError
  
          # check the router_id looks like an ipv4 address
          router_id = napalm.base.helpers.ip(router_id, version=4)
  
          # add parsed data to output dict
          bgp_neighbor_data["global"]["router_id"] = router_id
          bgp_neighbor_data["global"]["peers"] = {}
          for entry in summary_data:
              remote_addr = napalm.base.helpers.ip(entry["remote_addr"])
              afi = entry["afi"].lower()
              # check that we're looking at a supported afi
              if afi not in supported_afi:
                  continue
              # get neighbor_entry out of neighbor data
              neighbor_entry = None
              for neighbor in neighbor_data:
                  if (
                      neighbor["afi"].lower() == afi
                      and napalm.base.helpers.ip(neighbor["remote_addr"]) == remote_addr
                  ):
                      neighbor_entry = neighbor
                      break
              # check for proper session data for the afi
              if neighbor_entry is None:
                  continue
              elif not isinstance(neighbor_entry, dict):
                  raise ValueError(
                      msg="Couldn't find neighbor data for %s in afi %s"
                      % (remote_addr, afi)
                  )
  
              # check for admin down state
              try:
                  if "(Admin)" in entry["state"]:
                      is_enabled = False
                  else:
                      is_enabled = True
              except KeyError:
                  is_enabled = True
  
              # parse uptime value
              uptime = self.bgp_time_conversion(entry["uptime"])
  
              # BGP is up if state is Established
              is_up = "Established" in neighbor_entry["state"]
  
              # check whether session is up for address family and get prefix count
              try:
                  accepted_prefixes = int(entry["accepted_prefixes"])
              except (ValueError, KeyError):
                  accepted_prefixes = -1
  
              # Only parse neighbor detailed data if BGP session is-up
              if is_up:
                  try:
                      # overide accepted_prefixes with neighbor data if possible (since that's newer)
                      accepted_prefixes = int(neighbor_entry["accepted_prefixes"])
                  except (ValueError, KeyError):
                      pass
  
                  # try to get received prefix count, otherwise set to accepted_prefixes
                  received_prefixes = neighbor_entry.get(
                      "received_prefixes", accepted_prefixes
                  )
  
                  # try to get sent prefix count and convert to int, otherwise set to -1
                  sent_prefixes = int(neighbor_entry.get("sent_prefixes", -1))
              else:
                  received_prefixes = -1
                  sent_prefixes = -1
                  uptime = -1
  
              # get description
              try:
                  description = py23_compat.text_type(neighbor_entry["description"])
              except KeyError:
                  description = ""
  
              # check the remote router_id looks like an ipv4 address
              remote_id = napalm.base.helpers.ip(neighbor_entry["remote_id"], version=4)
  
              if remote_addr not in bgp_neighbor_data["global"]["peers"]:
                  bgp_neighbor_data["global"]["peers"][remote_addr] = {
                      "local_as": napalm.base.helpers.as_number(entry["local_as"]),
                      "remote_as": napalm.base.helpers.as_number(entry["remote_as"]),
                      "remote_id": remote_id,
                      "is_up": is_up,
                      "is_enabled": is_enabled,
                      "description": description,
                      "uptime": uptime,
                      "address_family": {
                          afi: {
                              "received_prefixes": received_prefixes,
                              "accepted_prefixes": accepted_prefixes,
                              "sent_prefixes": sent_prefixes,
                          }
                      },
                  }
              else:
                  # found previous data for matching remote_addr, but for different afi
                  existing = bgp_neighbor_data["global"]["peers"][remote_addr]
                  assert afi not in existing["address_family"]
                  # compare with existing values and croak if they don't match
                  assert existing["local_as"] == napalm.base.helpers.as_number(
                      entry["local_as"]
                  )
                  assert existing["remote_as"] == napalm.base.helpers.as_number(
                      entry["remote_as"]
                  )
                  assert existing["remote_id"] == remote_id
                  assert existing["is_enabled"] == is_enabled
                  assert existing["description"] == description
                  # merge other values in a sane manner
                  existing["is_up"] = existing["is_up"] or is_up
                  existing["uptime"] = max(existing["uptime"], uptime)
                  existing["address_family"][afi] = {
                      "received_prefixes": received_prefixes,
                      "accepted_prefixes": accepted_prefixes,
                      "sent_prefixes": sent_prefixes,
                  }
          return bgp_neighbor_data