From c5d00af8ff00f857cc8def9c91cb3dfe52759621 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Sun, 31 Mar 2024 11:52:28 +0200 Subject: [PATCH 01/15] Update vyos.py adding get_bgp_neighbors_detail --- napalm_vyos/vyos.py | 780 +++++++++++++++++++++++++------------------- 1 file changed, 453 insertions(+), 327 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index 23e3bde..be4fca7 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -19,23 +19,28 @@ Read napalm.readthedocs.org for more information. """ - -import re +import logging import os +import re import tempfile import vyattaconfparser +from django.core.cache import cache -from netmiko import __version__ as netmiko_version -from netmiko import ConnectHandler -from netmiko import SCPConn +cache.clear() # NAPALM base import napalm.base.constants as C from napalm.base.base import NetworkDriver -from napalm.base.exceptions import ConnectionException, MergeConfigException, \ - ReplaceConfigException, CommitError, \ - CommandErrorException +from napalm.base.exceptions import ( + CommitError, + ConnectionException, + MergeConfigException, + ReplaceConfigException, +) +from netmiko import ConnectHandler, SCPConn, __version__ as netmiko_version + +logger = logging.getLogger("peering.manager.peering") class VyOSDriver(NetworkDriver): @@ -62,44 +67,43 @@ class VyOSDriver(NetworkDriver): # Netmiko possible arguments netmiko_argument_map = { - 'port': None, - 'secret': '', - 'verbose': False, - 'global_delay_factor': 1, - 'use_keys': False, - 'key_file': None, - 'ssh_strict': False, - 'system_host_keys': False, - 'alt_host_keys': False, - 'alt_key_file': '', - 'ssh_config_file': None, + "port": None, + "secret": "", + "verbose": False, + "global_delay_factor": 1, + "use_keys": False, + "key_file": None, + "ssh_strict": False, + "system_host_keys": False, + "alt_host_keys": False, + "alt_key_file": "", + "ssh_config_file": None, } - fields = netmiko_version.split('.') + fields = netmiko_version.split(".") fields = [int(x) for x in fields] maj_ver, min_ver, bug_fix = fields - if maj_ver >= 2: - netmiko_argument_map['allow_agent'] = False - elif maj_ver == 1 and min_ver >= 1: - netmiko_argument_map['allow_agent'] = False - + if maj_ver >= 2 or maj_ver == 1 and min_ver >= 1: + netmiko_argument_map["allow_agent"] = False # Build dict of any optional Netmiko args self.netmiko_optional_args = {} if optional_args is not None: - for k, v in netmiko_argument_map.items(): + for k in netmiko_argument_map: try: self.netmiko_optional_args[k] = optional_args[k] except KeyError: pass - self.global_delay_factor = optional_args.get('global_delay_factor', 1) - self.port = optional_args.get('port', 22) + self.global_delay_factor = optional_args.get("global_delay_factor", 1) + self.port = optional_args.get("port", 22) def open(self): - self.device = ConnectHandler(device_type='vyos', - host=self.hostname, - username=self.username, - password=self.password, - **self.netmiko_optional_args) + self.device = ConnectHandler( + device_type="vyos", + host=self.hostname, + username=self.username, + password=self.password, + **self.netmiko_optional_args, + ) try: self._scp_client = SCPConn(self.device) @@ -111,9 +115,7 @@ class VyOSDriver(NetworkDriver): def is_alive(self): """Returns a flag with the state of the SSH connection.""" - return { - 'is_alive': self.device.remote_conn.transport.is_active() - } + return {"is_alive": self.device.remote_conn.transport.is_active()} def load_replace_candidate(self, filename=None, config=None): """ @@ -123,85 +125,75 @@ class VyOSDriver(NetworkDriver): support a replace using a configuration string. """ if not filename and not config: - raise ReplaceConfigException('filename or config param must be provided.') + raise ReplaceConfigException("filename or config param must be provided.") if filename is None: - temp_file = tempfile.NamedTemporaryFile(mode='w+') + temp_file = tempfile.NamedTemporaryFile(mode="w+") temp_file.write(config) temp_file.flush() cfg_filename = temp_file.name else: cfg_filename = filename - - if os.path.exists(cfg_filename) is True: self._scp_client.scp_transfer_file(cfg_filename, self._DEST_FILENAME) - self.device.send_command("cp "+self._BOOT_FILENAME+" "+self._BACKUP_FILENAME) - output_loadcmd = self.device.send_config_set(['load '+self._DEST_FILENAME]) + self.device.send_command(f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}") + output_loadcmd = self.device.send_config_set([f"load {self._DEST_FILENAME}"]) match_loaded = re.findall("Load complete.", output_loadcmd) - match_notchanged = re.findall("No configuration changes to commit", output_loadcmd) - match_failed = re.findall("Failed to parse specified config file", output_loadcmd) + match_notchanged = re.findall( + "No configuration changes to commit", output_loadcmd + ) + if match_failed := re.findall( + "Failed to parse specified config file", output_loadcmd + ): + raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") - if match_failed: - raise ReplaceConfigException("Failed replace config: " - + output_loadcmd) - - if not match_loaded: - if not match_notchanged: - raise ReplaceConfigException("Failed replace config: " - + output_loadcmd) + if not match_loaded and not match_notchanged: + raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") else: raise ReplaceConfigException("config file is not found") - def load_merge_candidate(self, filename=None, config=None): """ Only configuration in set-format is supported with load_merge_candidate. """ if not filename and not config: - raise MergeConfigException('filename or config param must be provided.') + raise MergeConfigException("filename or config param must be provided.") if filename is None: - temp_file = tempfile.NamedTemporaryFile(mode='w+') + temp_file = tempfile.NamedTemporaryFile(mode="w+") temp_file.write(config) temp_file.flush() cfg_filename = temp_file.name else: cfg_filename = filename - - if os.path.exists(cfg_filename) is True: - with open(cfg_filename) as f: - self.device.send_command("cp "+self._BOOT_FILENAME+" " - + self._BACKUP_FILENAME) - self._new_config = f.read() - cfg = [x for x in self._new_config.split("\n") if x] - output_loadcmd = self.device.send_config_set(cfg) - match_setfailed = re.findall("Delete failed", output_loadcmd) - match_delfailed = re.findall("Set failed", output_loadcmd) - - if match_setfailed or match_delfailed: - raise MergeConfigException("Failed merge config: " - + output_loadcmd) - else: + if os.path.exists(cfg_filename) is not True: raise MergeConfigException("config file is not found") + with open(cfg_filename) as f: + self.device.send_command(f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}") + self._new_config = f.read() + cfg = [x for x in self._new_config.split("\n") if x] + output_loadcmd = self.device.send_config_set(cfg) + match_setfailed = re.findall("Delete failed", output_loadcmd) + match_delfailed = re.findall("Set failed", output_loadcmd) + if match_setfailed or match_delfailed: + raise MergeConfigException(f"Failed merge config: {output_loadcmd}") def discard_config(self): self.device.exit_config_mode() def compare_config(self): - output_compare = self.device.send_config_set(['compare']) - match = re.findall("No changes between working and active configurations", - output_compare) - if match: + output_compare = self.device.send_config_set(["compare"]) + if match := re.findall( + "No changes between working and active configurations", output_compare + ): return "" else: - diff = ''.join(output_compare.splitlines(True)[1:-1]) - return diff + return "".join(output_compare.splitlines(True)[1:-1]) def commit_config(self, message=""): if message: @@ -211,10 +203,10 @@ class VyOSDriver(NetworkDriver): try: self.device.commit() - except ValueError: - raise CommitError("Failed to commit config on the device") + except ValueError as e: + raise CommitError("Failed to commit config on the device") from e - self.device.send_config_set(['save']) + self.device.send_config_set(["save"]) self.device.exit_config_mode() def rollback(self): @@ -223,13 +215,11 @@ class VyOSDriver(NetworkDriver): if filename is None: filename = self._BACKUP_FILENAME - output_loadcmd = self.device.send_config_set(['load '+filename]) - match = re.findall("Load complete.", output_loadcmd) - if not match: - raise ReplaceConfigException("Failed rollback config: " - + output_loadcmd) + output_loadcmd = self.device.send_config_set([f"load {filename}"]) + if match := re.findall("Load complete.", output_loadcmd): + self.device.send_config_set(["commit", "save"]) else: - self.device.send_config_set(['commit', 'save']) + raise ReplaceConfigException(f"Failed rollback config: {output_loadcmd}") def get_environment(self): """ @@ -238,7 +228,7 @@ class VyOSDriver(NetworkDriver): r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 0 61404 139624 139360 0 0 0 0 9 14 0 0 100 0 """ - output_cpu_list = list() + output_cpu_list = [] output_cpu = self.device.send_command("vmstat") output_cpu = str(output_cpu) output_cpu_list = output_cpu.split("\n") @@ -259,39 +249,25 @@ class VyOSDriver(NetworkDriver): output_ram = self.device.send_command("free").split("\n")[1] available_ram, used_ram = output_ram.split()[1:3] - environment = { - "fans": { - "invalid": { - "status": False - } - }, - "temperature": { - "invalid": { - "temperature": 0.0, - "is_alert": False, - "is_critical": False - } - }, - "power": { - "invalid": { - "status": True, - "capacity": 0.0, - "output": 0.0 - } - }, - "cpu": { - "0": { - "%usage": float(cpu) + return { + "fans": {"invalid": {"status": False}}, + "temperature": { + "invalid": { + "temperature": 0.0, + "is_alert": False, + "is_critical": False, + } + }, + "power": {"invalid": {"status": True, "capacity": 0.0, "output": 0.0}}, + "cpu": { + "0": {"%usage": float(cpu)}, + }, + "memory": { + "available_ram": int(available_ram), + "used_ram": int(used_ram), }, - }, - "memory": { - "available_ram": int(available_ram), - "used_ram": int(used_ram) - } } - return environment - def get_interfaces(self): """ "show interfaces" output example: @@ -312,15 +288,17 @@ class VyOSDriver(NetworkDriver): # 'match' example: # [("br0", "u", "D"), ("eth0", "u", "u"), ("eth1", "u", "u")...] - iface_state = {iface_name: {"State": state, "Link": link} for iface_name, - state, link in match} + iface_state = { + iface_name: {"State": state, "Link": link} + for iface_name, state, link in match + } output_conf = self.device.send_command("show configuration") # Convert the configuration to dictionary config = vyattaconfparser.parse_conf(output_conf) - iface_dict = dict() + iface_dict = {} for iface_type in config["interfaces"]: @@ -334,20 +312,18 @@ class VyOSDriver(NetworkDriver): speed = 0 hw_id = details.get("hw-id", "00:00:00:00:00:00") - is_up = (iface_state[iface_name]["Link"] == "u") - is_enabled = (iface_state[iface_name]["State"] == "u") + is_up = iface_state[iface_name]["Link"] == "u" + is_enabled = iface_state[iface_name]["State"] == "u" - iface_dict.update({ - iface_name: { - "is_up": bool(is_up), - "is_enabled": bool(is_enabled), + iface_dict[iface_name] = { + "is_up": is_up, + "is_enabled": is_enabled, "description": description, "last_flapped": float(-1), "mtu": -1, "speed": int(speed), "mac_address": hw_id, - } - }) + } return iface_dict @@ -375,24 +351,20 @@ class VyOSDriver(NetworkDriver): # Skip the header line output = output[1:-1] - arp_table = list() + arp_table = [] for line in output: line = line.split() # 'line' example: # ["10.129.2.254", "ether", "00:50:56:97:af:b1", "C", "eth0"] # [u'10.0.12.33', u'(incomplete)', u'eth1'] - if "incomplete" in line[1]: - macaddr = "00:00:00:00:00:00" - else: - macaddr = line[2] - + macaddr = "00:00:00:00:00:00" if "incomplete" in line[1] else line[2] arp_table.append( { - 'interface': line[-1], - 'mac': macaddr, - 'ip': line[0], - 'age': 0.0, + "interface": line[-1], + "mac": macaddr, + "ip": line[0], + "age": 0.0, } ) @@ -410,48 +382,58 @@ class VyOSDriver(NetworkDriver): output = self.device.send_command("ntpq -np") output = output.split("\n")[2:] - ntp_stats = list() + ntp_stats = [] for ntp_info in output: if len(ntp_info) > 0: - remote, refid, st, t, when, hostpoll, reachability, delay, offset, \ - jitter = ntp_info.split() + ( + remote, + refid, + st, + t, + when, + hostpoll, + reachability, + delay, + offset, + jitter, + ) = ntp_info.split() # 'remote' contains '*' if the machine synchronized with NTP server synchronized = "*" in remote match = re.search(r"(\d+\.\d+\.\d+\.\d+)", remote) - ip = match.group(1) + ip = match[1] - when = when if when != '-' else 0 + when = when if when != "-" else 0 - ntp_stats.append({ - "remote": ip, - "referenceid": refid, - "synchronized": bool(synchronized), - "stratum": int(st), - "type": t, - "when": when, - "hostpoll": int(hostpoll), - "reachability": int(reachability), - "delay": float(delay), - "offset": float(offset), - "jitter": float(jitter), - }) + ntp_stats.append( + { + "remote": ip, + "referenceid": refid, + "synchronized": synchronized, + "stratum": int(st), + "type": t, + "when": when, + "hostpoll": int(hostpoll), + "reachability": int(reachability), + "delay": float(delay), + "offset": float(offset), + "jitter": float(jitter), + } + ) return ntp_stats def get_ntp_peers(self): output = self.device.send_command("ntpq -np") output_peers = output.split("\n")[2:] - ntp_peers = dict() + ntp_peers = {} for line in output_peers: if len(line) > 0: match = re.search(r"(\d+\.\d+\.\d+\.\d+)\s+", line) - ntp_peers.update({ - match.group(1): {} - }) + ntp_peers[match[1]] = {} return ntp_peers @@ -470,29 +452,46 @@ class VyOSDriver(NetworkDriver): 192.168.1.3 4 64521 7132 7103 0 0 0 4d21h05m 0 192.168.1.4 4 64522 0 0 0 0 0 never Active """ - + logger.warning("GET BGP PEERS") output = self.device.send_command("show ip bgp summary") output = output.split("\n") - - match = re.search(r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", - output[0]) + logger.warning(output[2]) + match = re.search( + r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", + output[2], + ) if not match: + logger.warning("BGP neighbor parsing failed") return {} - router_id = match.group(1) - local_as = int(match.group(2)) + router_id = match[1] + local_as = int(match[2]) - bgp_neighbor_data = dict() - bgp_neighbor_data["global"] = dict() + bgp_neighbor_data = {"global": {}} bgp_neighbor_data["global"]["router_id"] = router_id bgp_neighbor_data["global"]["peers"] = {} # delete the header and empty element - bgp_info = [i.strip() for i in output[6:-2] if i] - + bgp_info = [i.strip() for i in output[9:-2] if i] + logger.warning("Got BGP information") + logger.warning("STRIPPING:") + logger.warning(bgp_info) for i in bgp_info: if len(i) > 0: - peer_id, bgp_version, remote_as, msg_rcvd, msg_sent, table_version, \ - in_queue, out_queue, up_time, state_prefix = i.split() + values = i.split() + ( + peer_id, + bgp_version, + remote_as, + msg_rcvd, + msg_sent, + table_version, + in_queue, + out_queue, + up_time, + state_prefix, + ) = values[:10] + + logger.warning("PEER ID: %s" % peer_id) is_enabled = "(Admin)" not in state_prefix @@ -500,7 +499,7 @@ class VyOSDriver(NetworkDriver): try: state_prefix = int(state_prefix) - received_prefixes = int(state_prefix) + received_prefixes = state_prefix is_up = True except ValueError: state_prefix = -1 @@ -514,6 +513,8 @@ class VyOSDriver(NetworkDriver): else: raise ValueError("BGP neighbor parsing failed") + logger.warning("SHOW IP BGP NEIGHBORS %s" % peer_id) + """ 'show ip bgp neighbors 192.168.1.1' output example: BGP neighbor is 192.168.1.1, remote AS 64519, local AS 64520, external link @@ -524,87 +525,238 @@ class VyOSDriver(NetworkDriver): 1 accepted prefixes ~~~ """ - bgp_detail = self.device.send_command("show ip bgp neighbors %s" % peer_id) + bgp_detail = self.device.send_command( + "show ip bgp neighbors %s" % peer_id + ) - match_rid = re.search(r"remote router ID (\d+\.\d+\.\d+\.\d+).*", bgp_detail) - remote_rid = match_rid.group(1) + match_rid = re.search( + r"remote router ID (\d+\.\d+\.\d+\.\d+).*", bgp_detail + ) + remote_rid = match_rid[1] - match_prefix_accepted = re.search(r"(\d+) accepted prefixes", bgp_detail) - accepted_prefixes = match_prefix_accepted.group(1) + match_prefix_accepted = re.search( + r"(\d+) accepted prefixes", bgp_detail + ) + accepted_prefixes = match_prefix_accepted[1] bgp_neighbor_data["global"]["peers"].setdefault(peer_id, {}) peer_dict = { "description": "", - "is_enabled": bool(is_enabled), - "local_as": int(local_as), + "is_enabled": is_enabled, + "local_as": local_as, "is_up": bool(is_up), "remote_id": remote_rid, + "remote_address": peer_id, "uptime": int(self._bgp_time_conversion(up_time)), - "remote_as": int(remote_as) + "remote_as": int(remote_as), } - af_dict = dict() - af_dict[address_family] = { - "sent_prefixes": int(-1), - "accepted_prefixes": int(accepted_prefixes), - "received_prefixes": int(received_prefixes) + af_dict = { + address_family: { + "sent_prefixes": -1, + "accepted_prefixes": int(accepted_prefixes), + "received_prefixes": int(received_prefixes), + } } - peer_dict["address_family"] = af_dict bgp_neighbor_data["global"]["peers"][peer_id] = peer_dict + logger.warning("Returning BGP data") + logger.warning(bgp_neighbor_data) + return bgp_neighbor_data + + import re + + def get_bgp_neighbors_detail(self, neighbor_address=""): + def search_and_group(pattern, text, default=None): + """Helper function to perform regex search and return the first group, or a default value""" + match = re.search(pattern, text) + return match[1] if match else default + + def search_and_int_group(pattern, text, default=0): + """Helper function to perform regex search and return the first group as an integer, or a default value""" + result = search_and_group(pattern, text, default) + return int(result) if result is not None else default + + output = self.device.send_command("show ip bgp summary").split("\n") + match = re.search( + r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", + output[2], + ) + if not match: + logger.warning("BGP neighbor parsing failed") + return {} + + _router_id, local_as = match.group(1), int(match[2]) + bgp_neighbor_data = {"global": {}} + bgp_info = [i.strip() for i in output[9:-2] if i] + + for i in bgp_info: + if len(i) > 0: + values = i.split() + ( + peer_id, + bgp_version, + remote_as, + msg_rcvd, + msg_sent, + table_version, + in_queue, + out_queue, + up_time, + state_prefix, + prefix_sent, + peer_desc, + ) = values[:12] + + if neighbor_address and peer_id != neighbor_address: + self.logger.warning(f"Skipping {peer_id}") + continue + + prefix_sent = ( + 0 + if "(Admin)" in state_prefix or "Idle" in state_prefix + else int(prefix_sent) if prefix_sent.isdigit() else 0 + ) + received_prefixes = int(state_prefix) if state_prefix.isdigit() else -1 + bgp_detail = self.device.send_command(f"show ip bgp neighbors {peer_id}") + + peer_dict = { + "up": search_and_group( + r" BGP state = (\S+), up for", bgp_detail + ) + == "Established", + "local_as": local_as, + "remote_as": int(remote_as), + "router_id": search_and_group( + r"local router ID (\d+\.\d+\.\d+\.\d+)", bgp_detail + ), + "local_address": search_and_group( + r"local router ID (\d+\.\d+\.\d+\.\d+)", bgp_detail + ), + "routing_table": search_and_group( + r"BGP version (\d+)", bgp_detail + ), + "local_address_configured": bool( + re.search(r"Local host: (\d+\.\d+\.\d+\.\d+)", bgp_detail) + ), + "local_port": search_and_group( + r"Local port: (\d+)", bgp_detail + ), + "remote_address": peer_id, + "remote_port": search_and_group( + r"Foreign port: (\d+)", bgp_detail + ), + "multihop": search_and_int_group( + r"External BGP neighbor may be up to (\d+)", bgp_detail + ) + > 1, + "multipath": -1, + "remove_private_as": -1, + "import_policy": search_and_group( + r" Route map for incoming advertisements is (\S+)", + bgp_detail, + ), + "export_policy": search_and_group( + r" Route map for outgoing advertisements is (\S+)", + bgp_detail, + ), + "input_messages": -1, + "output_messages": -1, + "input_updates": -1, + "output_updates": -1, + "connection_state": search_and_group( + r" BGP state = (\S+)", bgp_detail + ) + .replace(",", "") + .lower(), + "bgp_state": search_and_group( + r" BGP state = (\S+)", bgp_detail + ).replace(",", ""), + "previous_connection_state": -1, + "last_event": -1, + "suppress_4byte_as": -1, + "local_as_prepend": -1, + "holdtime": search_and_group( + r"Hold time is (\d+)", bgp_detail + ), + "configured_holdtime": search_and_int_group( + r"Configured hold time is (\d+)", bgp_detail, 0 + ), + "keepalive": search_and_group( + r"keepalive interval is (\d+)", bgp_detail + ), + "configured_keepalive": search_and_int_group( + r"keepalive interval is (\d+)", bgp_detail, 0 + ), + "active_prefix_count": -1, + "accepted_prefix_count": search_and_int_group( + r"(\d+) accepted prefixes", bgp_detail, -1 + ), + "suppressed_prefix_count": -1, + "advertised_prefix_count": prefix_sent, + "received_prefix_count": received_prefixes, + "flap_count": -1, + } + + remote_as_int = int(remote_as) + bgp_neighbor_data["global"].setdefault(remote_as_int, []).append( + peer_dict + ) return bgp_neighbor_data def _bgp_time_conversion(self, bgp_uptime): - # uptime_letters = set(["y", "w", "h", "d"]) - if "never" in bgp_uptime: return -1 + if "y" in bgp_uptime: + match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) + return ( + int(match[1]) * self._YEAR_SECONDS + + int(match[3]) * self._WEEK_SECONDS + + int(match[5]) * self._DAY_SECONDS + ) + elif "w" in bgp_uptime: + match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) + return ( + (int(match.group(1)) * self._WEEK_SECONDS) + + (int(match.group(3)) * self._DAY_SECONDS) + + (int(match.group(5)) * self._HOUR_SECONDS) + ) + elif "d" in bgp_uptime: + match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) + return ( + (int(match.group(1)) * self._DAY_SECONDS) + + (int(match.group(3)) * self._HOUR_SECONDS) + + (int(match.group(5)) * self._MINUTE_SECONDS) + ) else: - if "y" in bgp_uptime: - match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) - uptime = ((int(match.group(1)) * self._YEAR_SECONDS) + - (int(match.group(3)) * self._WEEK_SECONDS) + - (int(match.group(5)) * self._DAY_SECONDS)) - return uptime - elif "w" in bgp_uptime: - match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) - uptime = ((int(match.group(1)) * self._WEEK_SECONDS) + - (int(match.group(3)) * self._DAY_SECONDS) + - (int(match.group(5)) * self._HOUR_SECONDS)) - return uptime - elif "d" in bgp_uptime: - match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) - uptime = ((int(match.group(1)) * self._DAY_SECONDS) + - (int(match.group(3)) * self._HOUR_SECONDS) + - (int(match.group(5)) * self._MINUTE_SECONDS)) - return uptime - else: - hours, minutes, seconds = map(int, bgp_uptime.split(":")) - uptime = ((hours * self._HOUR_SECONDS) + - (minutes * self._MINUTE_SECONDS) + seconds) - return uptime + hours, minutes, seconds = map(int, bgp_uptime.split(":")) + return ( + (hours * self._HOUR_SECONDS) + + (minutes * self._MINUTE_SECONDS) + + seconds + ) def get_lldp_neighbors(self): # Multiple neighbors per port are not implemented # The show lldp neighbors commands lists port descriptions, not IDs output = self.device.send_command("show lldp neighbors detail") - pattern = r'''(?s)Interface: +(?P\S+), [^\n]+ + pattern = r"""(?s)Interface: +(?P\S+), [^\n]+ .+? +SysName: +(?P\S+) .+? - +PortID: +ifname (?P\S+)''' + +PortID: +ifname (?P\S+)""" def _get_interface(match): return [ { - 'hostname': match.group('hostname'), - 'port': match.group('port'), + "hostname": match.group("hostname"), + "port": match.group("port"), } ] return { - match.group('interface'): _get_interface(match) + match.group("interface"): _get_interface(match) for match in re.finditer(pattern, output) } @@ -627,11 +779,9 @@ class VyOSDriver(NetworkDriver): interfaces = re.findall(r"(\S+): <.*", output) # count = re.findall("(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+", output) count = re.findall(r"(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)", output) - counters = dict() + counters = {} - j = 0 - - for i in count: + for j, i in enumerate(count): if j % 2 == 0: rx_errors = i[2] rx_discards = i[3] @@ -640,24 +790,20 @@ class VyOSDriver(NetworkDriver): rx_multicast_packets = i[5] rx_broadcast_packets = -1 else: - counters.update({ - interfaces[j // 2]: { - "tx_errors": int(i[2]), - "tx_discards": int(i[3]), - "tx_octets": int(i[0]), - "tx_unicast_packets": int(i[1]), - "tx_multicast_packets": int(-1), - "tx_broadcast_packets": int(-1), - "rx_errors": int(rx_errors), - "rx_discards": int(rx_discards), - "rx_octets": int(rx_octets), - "rx_unicast_packets": int(rx_unicast_packets), - "rx_multicast_packets": int(rx_multicast_packets), - "rx_broadcast_packets": int(rx_broadcast_packets) - } - }) - j += 1 - + counters[interfaces[j // 2]] = { + "tx_errors": int(i[2]), + "tx_discards": int(i[3]), + "tx_octets": int(i[0]), + "tx_unicast_packets": int(i[1]), + "tx_multicast_packets": -1, + "tx_broadcast_packets": -1, + "rx_errors": int(rx_errors), + "rx_discards": int(rx_discards), + "rx_octets": int(rx_octets), + "rx_unicast_packets": int(rx_unicast_packets), + "rx_multicast_packets": int(rx_multicast_packets), + "rx_broadcast_packets": int(rx_broadcast_packets), + } return counters def get_snmp_information(self): @@ -667,23 +813,25 @@ class VyOSDriver(NetworkDriver): # convert the configuration to dictionary config = vyattaconfparser.parse_conf(output) - snmp = dict() - snmp["community"] = dict() - + snmp = {"community": {}} try: for i in config["service"]["snmp"]["community"]: - snmp["community"].update({ - i: { - "acl": "", - "mode": config["service"]["snmp"]["community"][i]["authorization"], + snmp["community"].update( + { + i: { + "acl": "", + "mode": config["service"]["snmp"]["community"][i][ + "authorization" + ], + } } - }) + ) - snmp.update({ - "chassis_id": "", - "contact": config["service"]["snmp"]["contact"], - "location": config["service"]["snmp"]["location"], - }) + snmp |= { + "chassis_id": "", + "contact": config["service"]["snmp"]["contact"], + "location": config["service"]["snmp"]["location"], + } return snmp except KeyError: @@ -698,7 +846,9 @@ class VyOSDriver(NetworkDriver): ver_str = [line for line in output if "Version" in line][0] version = self.parse_version(ver_str) - above_1_1 = False if version.startswith('1.0') or version.startswith('1.1') else True + above_1_1 = bool( + not version.startswith("1.0") and not version.startswith("1.1") + ) if above_1_1: sn_str = [line for line in output if "Hardware S/N" in line][0] hwmodel_str = [line for line in output if "Hardware model" in line][0] @@ -722,28 +872,27 @@ class VyOSDriver(NetworkDriver): else: fqdn = "" - iface_list = list() + iface_list = [] for iface_type in config["interfaces"]: for iface_name in config["interfaces"][iface_type]: iface_list.append(iface_name) facts = { - "uptime": int(uptime), - "vendor": "VyOS", - "os_version": version, - "serial_number": snumber, - "model": hwmodel, - "hostname": hostname, - "fqdn": fqdn, - "interface_list": iface_list, + "uptime": int(uptime), + "vendor": "VyOS", + "os_version": version, + "serial_number": snumber, + "model": hwmodel, + "hostname": hostname, + "fqdn": fqdn, + "interface_list": iface_list, } return facts @staticmethod def parse_version(ver_str): - version = ver_str.split()[-1] - return version + return ver_str.split()[-1] @staticmethod def parse_snumber(sn_str): @@ -765,7 +914,7 @@ class VyOSDriver(NetworkDriver): else: ifaces = [x for x in output[3:-1] if "-" not in x] - ifaces_ip = dict() + ifaces_ip = {} for iface in ifaces: iface = iface.split() @@ -776,14 +925,14 @@ class VyOSDriver(NetworkDriver): # Delete the "Interface" column iface = iface[1:-1] # Key initialization - ifaces_ip[iface_name] = dict() + ifaces_ip[iface_name] = {} ip_addr, mask = iface[0].split("/") ip_ver = self._get_ip_version(ip_addr) # Key initialization if ip_ver not in ifaces_ip[iface_name]: - ifaces_ip[iface_name][ip_ver] = dict() + ifaces_ip[iface_name][ip_ver] = {} ifaces_ip[iface_name][ip_ver][ip_addr] = {"prefix_length": int(mask)} @@ -802,12 +951,12 @@ class VyOSDriver(NetworkDriver): user_conf = [x.split() for x in output if "login user" in x] # Collect all users' name - user_name = list(set([x[4] for x in user_conf])) + user_name = list({x[4] for x in user_conf}) - user_auth = dict() + user_auth = {} for user in user_name: - sshkeys = list() + sshkeys = [] # extract the configuration which relates to 'user' for line in [x for x in user_conf if user in x]: @@ -816,56 +965,41 @@ class VyOSDriver(NetworkDriver): if line[6] == "encrypted-password": password = line[7].strip("'") - # set system login user alice level 'admin' elif line[5] == "level": - if line[6].strip("'") == "admin": - level = 15 - else: - level = 0 - - # "set system login user alice authentication public-keys - # alice@example.com key 'ABC'" + level = 15 if line[6].strip("'") == "admin" else 0 elif len(line) == 10 and line[8] == "key": sshkeys.append(line[9].strip("'")) - user_auth.update({ - user: { - "level": level, - "password": password, - "sshkeys": sshkeys - } - }) + user_auth[user] = {"level": level, "password": password, "sshkeys": sshkeys} return user_auth - def ping(self, - destination, - source=C.PING_SOURCE, - ttl=C.PING_TTL, - timeout=C.PING_TIMEOUT, - size=C.PING_SIZE, - count=C.PING_COUNT, - vrf=C.PING_VRF): + def ping( + self, + destination, + source=C.PING_SOURCE, + ttl=C.PING_TTL, + timeout=C.PING_TIMEOUT, + size=C.PING_SIZE, + count=C.PING_COUNT, + vrf=C.PING_VRF, + ): # does not support multiple destination yet deadline = timeout * count - command = "ping %s " % destination + command = f"ping {destination} " command += "ttl %d " % ttl command += "deadline %d " % deadline command += "size %d " % size command += "count %d " % count if source != "": - command += "interface %s " % source + command += f"interface {source} " - ping_result = dict() + ping_result = {} output_ping = self.device.send_command(command) - if "Unknown host" in output_ping: - err = "Unknown host" - else: - err = "" - + err = "Unknown host" if "Unknown host" in output_ping else "" if err: ping_result["error"] = err else: @@ -874,11 +1008,7 @@ class VyOSDriver(NetworkDriver): # 'loss,', 'time', '3997ms'] packet_info = output_ping.split("\n") - if len(packet_info[-1]) > 0: - packet_info = packet_info[-2] - else: - packet_info = packet_info[-3] - + packet_info = packet_info[-2] if len(packet_info[-1]) > 0 else packet_info[-3] packet_info = [x.strip() for x in packet_info.split()] sent = int(packet_info[0]) @@ -889,25 +1019,21 @@ class VyOSDriver(NetworkDriver): # ["0.307/0.396/0.480/0.061"] rtt_info = output_ping.split("\n") - if len(rtt_info[-1]) > 0: - rtt_info = rtt_info[-1] - else: - rtt_info = rtt_info[-2] - + rtt_info = rtt_info[-1] if len(rtt_info[-1]) > 0 else rtt_info[-2] match = re.search(r"([\d\.]+)/([\d\.]+)/([\d\.]+)/([\d\.]+)", rtt_info) if match is not None: - rtt_min = float(match.group(1)) - rtt_avg = float(match.group(2)) - rtt_max = float(match.group(3)) - rtt_stddev = float(match.group(4)) + rtt_min = float(match[1]) + rtt_avg = float(match[2]) + rtt_max = float(match[3]) + rtt_stddev = float(match[4]) else: rtt_min = None rtt_avg = None rtt_max = None rtt_stddev = None - ping_result["success"] = dict() + ping_result["success"] = {} ping_result["success"] = { "probes_sent": sent, "packet_loss": lost, @@ -915,7 +1041,7 @@ class VyOSDriver(NetworkDriver): "rtt_max": rtt_max, "rtt_avg": rtt_avg, "rtt_stddev": rtt_stddev, - "results": [{"ip_address": destination, "rtt": rtt_avg}] + "results": [{"ip_address": destination, "rtt": rtt_avg}], } return ping_result @@ -933,20 +1059,20 @@ class VyOSDriver(NetworkDriver): - startup(string) - Representation of the native startup configuration. """ if retrieve not in ["running", "candidate", "startup", "all"]: - raise Exception("ERROR: Not a valid option to retrieve.\nPlease select from 'running', 'candidate', " - "'startup', or 'all'") + raise Exception( + "ERROR: Not a valid option to retrieve.\nPlease select from 'running', 'candidate', " + "'startup', or 'all'" + ) else: - config_dict = { - "running": "", - "startup": "", - "candidate": "" - } + config_dict = {"running": "", "startup": "", "candidate": ""} if retrieve in ["running", "all"]: - config_dict['running'] = self._get_running_config(sanitized) + config_dict["running"] = self._get_running_config(sanitized) if retrieve in ["startup", "all"]: - config_dict['startup'] = self.device.send_command(f"cat {self._BOOT_FILENAME}") + config_dict["startup"] = self.device.send_command( + f"cat {self._BOOT_FILENAME}" + ) if retrieve in ["candidate", "all"]: - config_dict['candidate'] = self._new_config or "" + config_dict["candidate"] = self._new_config or "" return config_dict @@ -955,6 +1081,6 @@ class VyOSDriver(NetworkDriver): return self.device.send_command("show configuration") self.device.config_mode() config = self.device.send_command("show") - config = config[:config.rfind('\n')] + config = config[: config.rfind("\n")] self.device.exit_config_mode() return config From 9c16a0055a7645980b05c61d22705c42d449895b Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Sun, 31 Mar 2024 11:53:46 +0200 Subject: [PATCH 02/15] Update vyos.py Remove logging --- napalm_vyos/vyos.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index be4fca7..6ab5e12 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -25,9 +25,6 @@ import re import tempfile import vyattaconfparser -from django.core.cache import cache - -cache.clear() # NAPALM base import napalm.base.constants as C @@ -40,9 +37,6 @@ from napalm.base.exceptions import ( ) from netmiko import ConnectHandler, SCPConn, __version__ as netmiko_version -logger = logging.getLogger("peering.manager.peering") - - class VyOSDriver(NetworkDriver): _MINUTE_SECONDS = 60 @@ -452,16 +446,14 @@ class VyOSDriver(NetworkDriver): 192.168.1.3 4 64521 7132 7103 0 0 0 4d21h05m 0 192.168.1.4 4 64522 0 0 0 0 0 never Active """ - logger.warning("GET BGP PEERS") output = self.device.send_command("show ip bgp summary") output = output.split("\n") - logger.warning(output[2]) + match = re.search( r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", output[2], ) if not match: - logger.warning("BGP neighbor parsing failed") return {} router_id = match[1] local_as = int(match[2]) @@ -472,9 +464,7 @@ class VyOSDriver(NetworkDriver): # delete the header and empty element bgp_info = [i.strip() for i in output[9:-2] if i] - logger.warning("Got BGP information") - logger.warning("STRIPPING:") - logger.warning(bgp_info) + for i in bgp_info: if len(i) > 0: values = i.split() @@ -491,8 +481,6 @@ class VyOSDriver(NetworkDriver): state_prefix, ) = values[:10] - logger.warning("PEER ID: %s" % peer_id) - is_enabled = "(Admin)" not in state_prefix received_prefixes = None @@ -513,8 +501,6 @@ class VyOSDriver(NetworkDriver): else: raise ValueError("BGP neighbor parsing failed") - logger.warning("SHOW IP BGP NEIGHBORS %s" % peer_id) - """ 'show ip bgp neighbors 192.168.1.1' output example: BGP neighbor is 192.168.1.1, remote AS 64519, local AS 64520, external link @@ -560,8 +546,7 @@ class VyOSDriver(NetworkDriver): } peer_dict["address_family"] = af_dict bgp_neighbor_data["global"]["peers"][peer_id] = peer_dict - logger.warning("Returning BGP data") - logger.warning(bgp_neighbor_data) + return bgp_neighbor_data import re From d5a4d0d4222621e57752edc39bce35d79a799789 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Mon, 1 Apr 2024 16:30:09 +0200 Subject: [PATCH 03/15] Update vyos.py Migrate to templates --- napalm_vyos/vyos.py | 430 +++++++++++++++++++------------------------- 1 file changed, 181 insertions(+), 249 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index 6ab5e12..c08f77c 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -19,12 +19,15 @@ Read napalm.readthedocs.org for more information. """ -import logging import os import re import tempfile +import textfsm import vyattaconfparser +from django.core.cache import cache + +cache.clear() # NAPALM base import napalm.base.constants as C @@ -37,6 +40,7 @@ from napalm.base.exceptions import ( ) from netmiko import ConnectHandler, SCPConn, __version__ as netmiko_version + class VyOSDriver(NetworkDriver): _MINUTE_SECONDS = 60 @@ -129,24 +133,26 @@ class VyOSDriver(NetworkDriver): else: cfg_filename = filename - if os.path.exists(cfg_filename) is True: - self._scp_client.scp_transfer_file(cfg_filename, self._DEST_FILENAME) - self.device.send_command(f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}") - output_loadcmd = self.device.send_config_set([f"load {self._DEST_FILENAME}"]) - match_loaded = re.findall("Load complete.", output_loadcmd) - match_notchanged = re.findall( - "No configuration changes to commit", output_loadcmd - ) - if match_failed := re.findall( - "Failed to parse specified config file", output_loadcmd - ): - raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") - - if not match_loaded and not match_notchanged: - raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") - - else: + if os.path.exists(cfg_filename) is not True: raise ReplaceConfigException("config file is not found") + self._scp_client.scp_transfer_file(cfg_filename, self._DEST_FILENAME) + self.device.send_command( + f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}" + ) + output_loadcmd = self.device.send_config_set( + [f"load {self._DEST_FILENAME}"] + ) + match_loaded = re.findall("Load complete.", output_loadcmd) + match_notchanged = re.findall( + "No configuration changes to commit", output_loadcmd + ) + if match_failed := re.findall( + "Failed to parse specified config file", output_loadcmd + ): + raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") + + if not match_loaded and not match_notchanged: + raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") def load_merge_candidate(self, filename=None, config=None): """ @@ -167,7 +173,9 @@ class VyOSDriver(NetworkDriver): if os.path.exists(cfg_filename) is not True: raise MergeConfigException("config file is not found") with open(cfg_filename) as f: - self.device.send_command(f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}") + self.device.send_command( + f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}" + ) self._new_config = f.read() cfg = [x for x in self._new_config.split("\n") if x] output_loadcmd = self.device.send_config_set(cfg) @@ -213,7 +221,9 @@ class VyOSDriver(NetworkDriver): if match := re.findall("Load complete.", output_loadcmd): self.device.send_config_set(["commit", "save"]) else: - raise ReplaceConfigException(f"Failed rollback config: {output_loadcmd}") + raise ReplaceConfigException( + f"Failed rollback config: {output_loadcmd}" + ) def get_environment(self): """ @@ -446,249 +456,172 @@ class VyOSDriver(NetworkDriver): 192.168.1.3 4 64521 7132 7103 0 0 0 4d21h05m 0 192.168.1.4 4 64522 0 0 0 0 0 never Active """ + output = self.device.send_command("show ip bgp summary") - output = output.split("\n") - match = re.search( - r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", - output[2], - ) - if not match: - return {} - router_id = match[1] - local_as = int(match[2]) + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join(current_dir, "templates", "bgp_sum.template") - bgp_neighbor_data = {"global": {}} - bgp_neighbor_data["global"]["router_id"] = router_id - bgp_neighbor_data["global"]["peers"] = {} + # Assuming you've got a TextFSM template ready to parse the `bgp_detail` output + with open(template_path) as template_file: + fsm = textfsm.TextFSM(template_file) + header = fsm.header + result = fsm.ParseText(output) - # delete the header and empty element - bgp_info = [i.strip() for i in output[9:-2] if i] + bgp_neighbor_data = {"global": {"router_id": "", "peers": {}}} - for i in bgp_info: - if len(i) > 0: - values = i.split() - ( - peer_id, - bgp_version, - remote_as, - msg_rcvd, - msg_sent, - table_version, - in_queue, - out_queue, - up_time, - state_prefix, - ) = values[:10] + bgp_neighbor_data["global"]["router_id"] = result[0][ + header.index("BGP_ROUTER_ID") + ] - is_enabled = "(Admin)" not in state_prefix + for neighbor in result: + peer_id = neighbor[header.index("NEIGHBOR")] + bgp_neighbor_data["global"]["peers"][peer_id] = [] - received_prefixes = None + peer_dict = { + "description": str(neighbor[header.index("DESCRIPTION")]), + "is_enabled": "Admin" not in neighbor[header.index("PREFIX_SENT")], + "local_as": int(neighbor[header.index("LOCAL_AS")]), + "is_up": "Active" + not in neighbor[header.index("STATE_PREFIX_RECEIVED")], + "remote_id": neighbor[header.index("NEIGHBOR")], + "remote_address": neighbor[header.index("NEIGHBOR")], + "uptime": int( + self._bgp_time_conversion(neighbor[header.index("UP_TIME")]) + ), + "remote_as": int(neighbor[header.index("NEIGHBOR_AS")]), + } - try: - state_prefix = int(state_prefix) - received_prefixes = state_prefix - is_up = True - except ValueError: - state_prefix = -1 - received_prefixes = -1 - is_up = False - - if bgp_version == "4": - address_family = "ipv4" - elif bgp_version == "6": - address_family = "ipv6" - else: - raise ValueError("BGP neighbor parsing failed") - - """ - 'show ip bgp neighbors 192.168.1.1' output example: - BGP neighbor is 192.168.1.1, remote AS 64519, local AS 64520, external link - BGP version 4, remote router ID 192.168.1.1 - For address family: IPv4 Unicast - ~~~ - Community attribute sent to this neighbor(both) - 1 accepted prefixes - ~~~ - """ - bgp_detail = self.device.send_command( - "show ip bgp neighbors %s" % peer_id - ) - - match_rid = re.search( - r"remote router ID (\d+\.\d+\.\d+\.\d+).*", bgp_detail - ) - remote_rid = match_rid[1] - - match_prefix_accepted = re.search( - r"(\d+) accepted prefixes", bgp_detail - ) - accepted_prefixes = match_prefix_accepted[1] - - bgp_neighbor_data["global"]["peers"].setdefault(peer_id, {}) - peer_dict = { - "description": "", - "is_enabled": is_enabled, - "local_as": local_as, - "is_up": bool(is_up), - "remote_id": remote_rid, - "remote_address": peer_id, - "uptime": int(self._bgp_time_conversion(up_time)), - "remote_as": int(remote_as), - } - - af_dict = { - address_family: { - "sent_prefixes": -1, - "accepted_prefixes": int(accepted_prefixes), - "received_prefixes": int(received_prefixes), - } - } - peer_dict["address_family"] = af_dict - bgp_neighbor_data["global"]["peers"][peer_id] = peer_dict + bgp_neighbor_data["global"]["peers"][peer_id] = peer_dict return bgp_neighbor_data - import re - def get_bgp_neighbors_detail(self, neighbor_address=""): - def search_and_group(pattern, text, default=None): - """Helper function to perform regex search and return the first group, or a default value""" - match = re.search(pattern, text) - return match[1] if match else default - def search_and_int_group(pattern, text, default=0): - """Helper function to perform regex search and return the first group as an integer, or a default value""" - result = search_and_group(pattern, text, default) - return int(result) if result is not None else default + def safe_int(value, default=0): + try: + return int(value) if value and value.isdigit() else default + except ValueError: + return default - output = self.device.send_command("show ip bgp summary").split("\n") - match = re.search( - r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", - output[2], - ) - if not match: - logger.warning("BGP neighbor parsing failed") - return {} + neighbors = self.get_bgp_neighbors() - _router_id, local_as = match.group(1), int(match[2]) - bgp_neighbor_data = {"global": {}} - bgp_info = [i.strip() for i in output[9:-2] if i] + for neighbor in neighbors["global"]["peers"]: - for i in bgp_info: - if len(i) > 0: - values = i.split() - ( - peer_id, - bgp_version, - remote_as, - msg_rcvd, - msg_sent, - table_version, - in_queue, - out_queue, - up_time, - state_prefix, - prefix_sent, - peer_desc, - ) = values[:12] + output = self.device.send_command(f"show ip bgp neighbor {neighbor}") - if neighbor_address and peer_id != neighbor_address: - self.logger.warning(f"Skipping {peer_id}") + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join( + current_dir, "templates", "bgp_details.template" + ) + + bgp_neighbor_data = {"global": {}} + + with open(template_path) as template_file: + fsm = textfsm.TextFSM(template_file) + result = fsm.ParseText(output) + + if not result: continue - prefix_sent = ( - 0 - if "(Admin)" in state_prefix or "Idle" in state_prefix - else int(prefix_sent) if prefix_sent.isdigit() else 0 - ) - received_prefixes = int(state_prefix) if state_prefix.isdigit() else -1 - bgp_detail = self.device.send_command(f"show ip bgp neighbors {peer_id}") + neighbors_dicts = [ + dict(zip(fsm.header, neighbor)) for neighbor in result + ] - peer_dict = { - "up": search_and_group( - r" BGP state = (\S+), up for", bgp_detail - ) - == "Established", - "local_as": local_as, - "remote_as": int(remote_as), - "router_id": search_and_group( - r"local router ID (\d+\.\d+\.\d+\.\d+)", bgp_detail - ), - "local_address": search_and_group( - r"local router ID (\d+\.\d+\.\d+\.\d+)", bgp_detail - ), - "routing_table": search_and_group( - r"BGP version (\d+)", bgp_detail - ), - "local_address_configured": bool( - re.search(r"Local host: (\d+\.\d+\.\d+\.\d+)", bgp_detail) - ), - "local_port": search_and_group( - r"Local port: (\d+)", bgp_detail - ), - "remote_address": peer_id, - "remote_port": search_and_group( - r"Foreign port: (\d+)", bgp_detail - ), - "multihop": search_and_int_group( - r"External BGP neighbor may be up to (\d+)", bgp_detail - ) - > 1, - "multipath": -1, - "remove_private_as": -1, - "import_policy": search_and_group( - r" Route map for incoming advertisements is (\S+)", - bgp_detail, - ), - "export_policy": search_and_group( - r" Route map for outgoing advertisements is (\S+)", - bgp_detail, - ), - "input_messages": -1, - "output_messages": -1, - "input_updates": -1, - "output_updates": -1, - "connection_state": search_and_group( - r" BGP state = (\S+)", bgp_detail - ) - .replace(",", "") - .lower(), - "bgp_state": search_and_group( - r" BGP state = (\S+)", bgp_detail - ).replace(",", ""), - "previous_connection_state": -1, - "last_event": -1, - "suppress_4byte_as": -1, - "local_as_prepend": -1, - "holdtime": search_and_group( - r"Hold time is (\d+)", bgp_detail - ), - "configured_holdtime": search_and_int_group( - r"Configured hold time is (\d+)", bgp_detail, 0 - ), - "keepalive": search_and_group( - r"keepalive interval is (\d+)", bgp_detail - ), - "configured_keepalive": search_and_int_group( - r"keepalive interval is (\d+)", bgp_detail, 0 - ), - "active_prefix_count": -1, - "accepted_prefix_count": search_and_int_group( - r"(\d+) accepted prefixes", bgp_detail, -1 - ), - "suppressed_prefix_count": -1, - "advertised_prefix_count": prefix_sent, - "received_prefix_count": received_prefixes, - "flap_count": -1, - } + for neighbor in neighbors_dicts: - remote_as_int = int(remote_as) - bgp_neighbor_data["global"].setdefault(remote_as_int, []).append( - peer_dict - ) + remote_as = neighbor["REMOTE_AS"] - return bgp_neighbor_data + peer_dict = { + "up": neighbor["BGP_STATE"].lower() == "established", + "local_as": int(neighbor["LOCAL_AS"]), + "remote_as": int(neighbor["REMOTE_AS"]), + "router_id": neighbor["LOCAL_ROUTER_ID"], + "local_address": neighbor[ + "LOCAL_ROUTER_ID" + ], # Adjusted from LOCAL_ROUTER_ID based on context + "routing_table": f"IPv{neighbor['BGP_VERSION']} Unicast", # Constructed value + "local_address_configured": bool(neighbor["LOCAL_ROUTER_ID"]), + "local_port": ( + int(neighbor["LOCAL_PORT"]) + if neighbor["LOCAL_PORT"].isdigit() + else None + ), + "remote_address": neighbor["REMOTE_ROUTER_ID"], + "remote_port": neighbor["FOREIGN_PORT"], + "multipath": neighbor.get( + "DYNAMIC_CAPABILITY", "no" + ), # Assuming DYNAMIC_CAPABILITY indicates multipath + "remove_private_as": ( + "yes" + if neighbor.get("REMOVE_PRIVATE_AS", "no") != "no" + else "no" + ), # Placeholder for actual value + "input_messages": sum( + int(neighbor["MESSAGE_STATISTICS_RECEIVED"][i]) + for i in range(len(neighbor["MESSAGE_STATISTICS_TYPE"])) + if neighbor["MESSAGE_STATISTICS_TYPE"][i] + in ["Updates", "Keepalives"] + ), + "output_messages": sum( + int(neighbor["MESSAGE_STATISTICS_SENT"][i]) + for i in range(len(neighbor["MESSAGE_STATISTICS_TYPE"])) + if neighbor["MESSAGE_STATISTICS_TYPE"][i] + in ["Updates", "Keepalives"] + ), + "input_updates": safe_int( + neighbor.get("RECEIVED_PREFIXES_IPV4") + ) + + safe_int(neighbor.get("RECEIVED_PREFIXES_IPV6")), + "output_updates": safe_int( + neighbor.get("ADVERTISED_PREFIX_COUNT") + ), + "connection_state": neighbor["BGP_STATE"].lower(), + "bgp_state": neighbor["BGP_STATE"].lower(), + "previous_connection_state": neighbor.get( + "LAST_RESET_REASON", "unknown" + ), + "last_event": neighbor.get( + "LAST_EVENT", "Not Available" + ), # Assuming LAST_EVENT is available + "suppress_4byte_as": neighbor.get( + "FOUR_BYTE_AS_CAPABILITY", "Not Configured" + ), + "local_as_prepend": neighbor.get( + "LOCAL_AS_PREPEND", "Not Configured" + ), # Assuming LOCAL_AS_PREPEND is available + "holdtime": int(neighbor["HOLD_TIME"]), + "configured_holdtime": int(neighbor["CONFIGURED_HOLD_TIME"]), + "keepalive": int(neighbor["KEEPALIVE_INTERVAL"]), + "configured_keepalive": int( + neighbor["CONFIGURED_KEEPALIVE_INTERVAL"] + ), + "active_prefix_count": int( + neighbor.get("ACTIVE_PREFIX_COUNT", 0) + ), # Assuming ACTIVE_PREFIX_COUNT is available + "accepted_prefix_count": int( + neighbor.get("ACCEPTED_PREFIX_COUNT", 0) + ), # Assuming ACCEPTED_PREFIX_COUNT is available + "suppressed_prefix_count": int( + neighbor.get("SUPPRESSED_PREFIX_COUNT", 0) + ), # Assuming SUPPRESSED_PREFIX_COUNT is available + "advertised_prefix_count": int( + neighbor.get("ADVERTISED_PREFIX_COUNT", 0) + ), + "received_prefix_count": safe_int( + neighbor.get("RECEIVED_PREFIXES_IPV4", 0) + ) + + safe_int(neighbor.get("RECEIVED_PREFIXES_IPV6", 0)), + "flap_count": safe_int( + neighbor.get("FLAP_COUNT", 0) + ), # Assuming FLAP_COUNT is available + } + + bgp_neighbor_data["global"].setdefault(int(remote_as), []).append( + peer_dict + ) + + return bgp_neighbor_data def _bgp_time_conversion(self, bgp_uptime): if "never" in bgp_uptime: @@ -703,10 +636,9 @@ class VyOSDriver(NetworkDriver): elif "w" in bgp_uptime: match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) return ( - (int(match.group(1)) * self._WEEK_SECONDS) - + (int(match.group(3)) * self._DAY_SECONDS) - + (int(match.group(5)) * self._HOUR_SECONDS) - ) + int(match[1]) * self._WEEK_SECONDS + + int(match[3]) * self._DAY_SECONDS + ) + int(match[5]) * self._HOUR_SECONDS elif "d" in bgp_uptime: match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) return ( @@ -831,9 +763,7 @@ class VyOSDriver(NetworkDriver): ver_str = [line for line in output if "Version" in line][0] version = self.parse_version(ver_str) - above_1_1 = bool( - not version.startswith("1.0") and not version.startswith("1.1") - ) + above_1_1 = not version.startswith("1.0") and not version.startswith("1.1") if above_1_1: sn_str = [line for line in output if "Hardware S/N" in line][0] hwmodel_str = [line for line in output if "Hardware model" in line][0] @@ -993,7 +923,9 @@ class VyOSDriver(NetworkDriver): # 'loss,', 'time', '3997ms'] packet_info = output_ping.split("\n") - packet_info = packet_info[-2] if len(packet_info[-1]) > 0 else packet_info[-3] + packet_info = ( + packet_info[-2] if len(packet_info[-1]) > 0 else packet_info[-3] + ) packet_info = [x.strip() for x in packet_info.split()] sent = int(packet_info[0]) From 63a8cb72ba4f659def7b6fa3eeac51fb75e198d1 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Mon, 1 Apr 2024 16:30:31 +0200 Subject: [PATCH 04/15] Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index ebbf543..12fd5a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ napalm>=3.0 paramiko netmiko>=3.1.0 vyattaconfparser +textfsm From 03075301c822bed503a4c1418e59b1f8af49326d Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Mon, 13 May 2024 09:37:38 +0200 Subject: [PATCH 05/15] Create bgp_details.template --- napalm_vyos/templates/bgp_details.template | 146 +++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 napalm_vyos/templates/bgp_details.template diff --git a/napalm_vyos/templates/bgp_details.template b/napalm_vyos/templates/bgp_details.template new file mode 100644 index 0000000..690380e --- /dev/null +++ b/napalm_vyos/templates/bgp_details.template @@ -0,0 +1,146 @@ +Value NEIGHBOR (\S+) +Value REMOTE_AS (\d+) +Value LOCAL_AS (\d+) +Value LINK_TYPE (\w+ link) +Value LOCAL_ROLE (.+) +Value REMOTE_ROLE (.+) +Value DESCRIPTION (.+) +Value PEER_GROUP (\S+) +Value BGP_VERSION (\d+) +Value REMOTE_ROUTER_ID (\S+) +Value LOCAL_ROUTER_ID (\S+) +Value BGP_STATE (\S+) +Value UP_TIME (\S+) +Value HOLD_TIME (\d+) +Value KEEPALIVE_INTERVAL (\d+) +Value CONFIGURED_HOLD_TIME (\d+) +Value CONFIGURED_KEEPALIVE_INTERVAL (\d+) +Value CONFIGURED_CONDITIONAL_ADVERTISEMENTS_INTERVAL (\d+) +Value FOUR_BYTE_AS_CAPABILITY (\S+) +Value EXTENDED_MESSAGE_CAPABILITY (\S+) +Value IPV4_UNICAST_ADDPATH (\S+) +Value IPV6_UNICAST_ADDPATH (\S+) +Value DYNAMIC_CAPABILITY (\S+) +Value LONG_LIVED_GRACEFUL_RESTART_CAPABILITY (\S+) +Value ROUTE_REFRESH_CAPABILITY (\S+) +Value ENHANCED_ROUTE_REFRESH_CAPABILITY (\S+) +Value ADDRESS_FAMILY_IPV4_UNICAST_CAPABILITY (\S+) +Value ADDRESS_FAMILY_IPV6_UNICAST_CAPABILITY (\S+) +Value HOSTNAME_CAPABILITY (\S+) +Value VERSION_CAPABILITY (\S+) +Value GRACEFUL_RESTART_CAPABILITY (\S+) +Value REMOTE_RESTART_TIME (\d+) +Value END_OF_RIB_SEND_IPV4_UNICAST (\S+) +Value END_OF_RIB_RECEIVED_IPV4_UNICAST (\S+) +Value END_OF_RIB_SEND_IPV6_UNICAST (.*) +Value END_OF_RIB_RECEIVED_IPV6_UNICAST (.*) +Value LOCAL_GR_MODE (\S+) +Value REMOTE_GR_MODE (\S+) +Value R_BIT (\S+) +Value N_BIT (\S+) +Value CONFIGURED_RESTART_TIME (\d+) +Value RECEIVED_RESTART_TIME (\d+) +Value IPV4_UNICAST_F_BIT (\S+) +Value IPV6_UNICAST_F_BIT (\S+) +Value END_OF_RIB_SENT (\S+) +Value END_OF_RIB_SENT_AFTER_UPDATE (\S+) +Value END_OF_RIB_RECEIVED (\S+) +Value CONFIGURED_STALE_PATH_TIME (\d+) +Value MESSAGE_STATISTICS_INQ_DEPTH (\d+) +Value MESSAGE_STATISTICS_OUTQ_DEPTH (\d+) +Value List MESSAGE_STATISTICS_TYPE (\S+) +Value List MESSAGE_STATISTICS_SENT (\d+) +Value List MESSAGE_STATISTICS_RECEIVED (\d+) +Value RECEIVED_PREFIXES_IPV4 (\d+) +Value RECEIVED_PREFIXES_IPV6 (\d+) +Value ESTABLISHED_CONNECTIONS (\d+) +Value DROPPED_CONNECTIONS (\d+) +Value LAST_RESET (\S+) +Value LAST_RESET_REASON (.+) +Value LOCAL_HOST (\S+) +Value LOCAL_PORT (\d+) +Value FOREIGN_HOST (\S+) +Value FOREIGN_PORT (\d+) +Value NEXTHOP (\S+) +Value NEXTHOP_GLOBAL (\S+) +Value NEXTHOP_LOCAL (\S+) +Value BGP_CONNECTION_TYPE (\S+) +Value BGP_CONNECT_RETRY_TIMER (\d+) +Value ESTIMATED_RTT (\d+) + +Start + ^BGP neighbor is ${NEIGHBOR}, remote AS ${REMOTE_AS}, local AS ${LOCAL_AS}, ${LINK_TYPE} -> Neighbors + +Neighbors + ^\s*Local Role: ${LOCAL_ROLE} -> Continue + ^\s*Remote Role: ${REMOTE_ROLE} -> Continue + ^\s*Description: ${DESCRIPTION} -> Continue + ^\s*Member of peer-group ${PEER_GROUP} for session parameters -> Continue + ^\s*BGP version ${BGP_VERSION}, remote router ID ${REMOTE_ROUTER_ID}, local router ID ${LOCAL_ROUTER_ID} -> Continue + ^\s*BGP state = ${BGP_STATE}, up for ${UP_TIME} -> Continue + ^\s*BGP state = ${BGP_STATE} -> Continue + ^\s*Hold time is ${HOLD_TIME} seconds, keepalive interval is ${KEEPALIVE_INTERVAL} seconds -> Continue + ^\s*Configured hold time is ${CONFIGURED_HOLD_TIME} seconds, keepalive interval is ${CONFIGURED_KEEPALIVE_INTERVAL} seconds -> Continue + ^\s*Configured conditional advertisements interval is ${CONFIGURED_CONDITIONAL_ADVERTISEMENTS_INTERVAL} seconds -> Capabilities + +Capabilities + ^\s*4 Byte AS: ${FOUR_BYTE_AS_CAPABILITY} -> Continue + ^\s*Extended Message: ${EXTENDED_MESSAGE_CAPABILITY} -> Continue + ^\s*AddPath: -> AddPath + ^\s*Dynamic: ${DYNAMIC_CAPABILITY} -> Continue + ^\s*Long-lived Graceful Restart: ${LONG_LIVED_GRACEFUL_RESTART_CAPABILITY} -> Continue + ^\s*Route refresh: ${ROUTE_REFRESH_CAPABILITY} -> Continue + ^\s*Enhanced Route Refresh: ${ENHANCED_ROUTE_REFRESH_CAPABILITY} -> Continue + ^\s*Address Family IPv4 Unicast: ${ADDRESS_FAMILY_IPV4_UNICAST_CAPABILITY} -> Continue + ^\s*Address Family IPv6 Unicast: ${ADDRESS_FAMILY_IPV6_UNICAST_CAPABILITY} -> Continue + ^\s*Hostname Capability: ${HOSTNAME_CAPABILITY} -> Continue + ^\s*Version Capability: ${VERSION_CAPABILITY} -> Continue + ^\s*Graceful Restart Capability: ${GRACEFUL_RESTART_CAPABILITY} -> GR + +AddPath + ^\s*IPv4 Unicast: ${IPV4_UNICAST_ADDPATH} -> Capabilities + ^\s*IPv6 Unicast: ${IPV6_UNICAST_ADDPATH} -> Capabilities + +GR + ^\s*Remote Restart timer is ${REMOTE_RESTART_TIME} seconds -> Continue + ^\s*End-of-RIB send: ${END_OF_RIB_SEND_IPV4_UNICAST} -> Continue + ^\s*End-of-RIB received: ${END_OF_RIB_RECEIVED_IPV4_UNICAST} -> Continue + ^\s*End-of-RIB send: ${END_OF_RIB_SEND_IPV6_UNICAST} -> Continue + ^\s*End-of-RIB received: ${END_OF_RIB_RECEIVED_IPV6_UNICAST} -> Continue + ^\s*Local GR Mode: ${LOCAL_GR_MODE} -> Continue + ^\s*Remote GR Mode: ${REMOTE_GR_MODE} -> Continue + ^\s*R bit: ${R_BIT} -> Continue + ^\s*N bit: ${N_BIT} -> Timers + +Timers + ^\s*Configured Restart Time\(sec\): ${CONFIGURED_RESTART_TIME} -> Continue + ^\s*Received Restart Time\(sec\): ${RECEIVED_RESTART_TIME} -> Statistics + +Statistics + ^\s*Message statistics: -> Continue + ^\s*Inq depth is ${MESSAGE_STATISTICS_INQ_DEPTH} -> Continue + ^\s*Outq depth is ${MESSAGE_STATISTICS_OUTQ_DEPTH} -> Continue + ^\s*${MESSAGE_STATISTICS_TYPE}:\s+${MESSAGE_STATISTICS_SENT}\s+${MESSAGE_STATISTICS_RECEIVED} -> AFI + +AFI + ^\s*For address family: IPv4 Unicast -> AFI_IPv4 + ^\s*For address family: IPv6 Unicast -> AFI_IPv6 + +AFI_IPv4 + ^\s*ROUTESERVERS_V4 peer-group member -> Continue + ^\s*${RECEIVED_PREFIXES_IPV4} accepted prefixes -> ConnectionDetails + +AFI_IPv6 + ^\s*${RECEIVED_PREFIXES_IPV6} accepted prefixes -> ConnectionDetails + +ConnectionDetails + ^\s*Connections established ${ESTABLISHED_CONNECTIONS}; dropped ${DROPPED_CONNECTIONS} -> Continue + ^\s*Last reset ${LAST_RESET},\s+${LAST_RESET_REASON} -> Continue + ^\s*Local host: ${LOCAL_HOST}, Local port: ${LOCAL_PORT} -> Continue + ^\s*Foreign host: ${FOREIGN_HOST}, Foreign port: ${FOREIGN_PORT} -> Continue + ^\s*Nexthop: ${NEXTHOP} -> Continue + ^\s*Nexthop global: ${NEXTHOP_GLOBAL} -> Continue + ^\s*Nexthop local: ${NEXTHOP_LOCAL} -> Continue + ^\s*BGP connection: ${BGP_CONNECTION_TYPE} -> Continue + ^\s*BGP Connect Retry Timer in Seconds: ${BGP_CONNECT_RETRY_TIMER} -> Continue + ^\s*Estimated round trip time: ${ESTIMATED_RTT} ms -> Record From 9d037d21d18ba5209730e6c5734150a6b24016a1 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Mon, 13 May 2024 09:37:52 +0200 Subject: [PATCH 06/15] Create bgp_sum.template --- napalm_vyos/templates/bgp_sum.template | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 napalm_vyos/templates/bgp_sum.template diff --git a/napalm_vyos/templates/bgp_sum.template b/napalm_vyos/templates/bgp_sum.template new file mode 100644 index 0000000..023967e --- /dev/null +++ b/napalm_vyos/templates/bgp_sum.template @@ -0,0 +1,25 @@ +Value Filldown BGP_ROUTER_ID (\S+) +Value Filldown LOCAL_AS (\d+) +Value Filldown VRF_ID (\d+) +Value Required NEIGHBOR (\S+) +Value Required NEIGHBOR_AS (\d+) +Value Required MSG_RECEIVED (\d+) +Value Required MSG_SENT (\d+) +Value Required TABLE_VERSION (\d+) +Value Required IN_Q (\d+) +Value Required OUT_Q (\d+) +Value Required UP_TIME (\S+) +Value Required STATE_PREFIX_RECEIVED (.*?) +Value Required PREFIX_SENT (\d+) +Value Required DESCRIPTION (.+) + +Start + ^BGP router identifier ${BGP_ROUTER_ID}, local AS number ${LOCAL_AS} vrf-id ${VRF_ID} -> Neighbors + +Neighbors + ^Neighbor\s+V\s+AS\s+MsgRcvd\s+MsgSent\s+TblVer\s+InQ\s+OutQ\s+Up/Down\s+State/PfxRcd\s+PfxSnt\s+Desc -> PeerLine + +PeerLine + ^\s*$$ -> Start + ^${NEIGHBOR}\s+\d+\s+${NEIGHBOR_AS}\s+${MSG_RECEIVED}\s+${MSG_SENT}\s+${TABLE_VERSION}\s+${IN_Q}\s+${OUT_Q}\s+${UP_TIME}\s+${STATE_PREFIX_RECEIVED}\s+${PREFIX_SENT}\s+${DESCRIPTION} -> Record PeerLine + ^. -> Start From 422e64b671e15de747f8437f073f2b8b2996e616 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Mon, 13 May 2024 09:38:10 +0200 Subject: [PATCH 07/15] Update vyos.py --- napalm_vyos/vyos.py | 832 ++++++++++++++++++++++++-------------------- 1 file changed, 451 insertions(+), 381 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index 23e3bde..b4a56fc 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -19,23 +19,27 @@ Read napalm.readthedocs.org for more information. """ - -import re import os +import re import tempfile - +import logging +import textfsm import vyattaconfparser +logger = logging.getLogger("peering.manager.peering") +from django.core.cache import cache -from netmiko import __version__ as netmiko_version -from netmiko import ConnectHandler -from netmiko import SCPConn +cache.clear() # NAPALM base import napalm.base.constants as C from napalm.base.base import NetworkDriver -from napalm.base.exceptions import ConnectionException, MergeConfigException, \ - ReplaceConfigException, CommitError, \ - CommandErrorException +from napalm.base.exceptions import ( + CommitError, + ConnectionException, + MergeConfigException, + ReplaceConfigException, +) +from netmiko import ConnectHandler, SCPConn, __version__ as netmiko_version class VyOSDriver(NetworkDriver): @@ -62,44 +66,43 @@ class VyOSDriver(NetworkDriver): # Netmiko possible arguments netmiko_argument_map = { - 'port': None, - 'secret': '', - 'verbose': False, - 'global_delay_factor': 1, - 'use_keys': False, - 'key_file': None, - 'ssh_strict': False, - 'system_host_keys': False, - 'alt_host_keys': False, - 'alt_key_file': '', - 'ssh_config_file': None, + "port": None, + "secret": "", + "verbose": False, + "global_delay_factor": 1, + "use_keys": False, + "key_file": None, + "ssh_strict": False, + "system_host_keys": False, + "alt_host_keys": False, + "alt_key_file": "", + "ssh_config_file": None, } - fields = netmiko_version.split('.') + fields = netmiko_version.split(".") fields = [int(x) for x in fields] maj_ver, min_ver, bug_fix = fields - if maj_ver >= 2: - netmiko_argument_map['allow_agent'] = False - elif maj_ver == 1 and min_ver >= 1: - netmiko_argument_map['allow_agent'] = False - + if maj_ver >= 2 or maj_ver == 1 and min_ver >= 1: + netmiko_argument_map["allow_agent"] = False # Build dict of any optional Netmiko args self.netmiko_optional_args = {} if optional_args is not None: - for k, v in netmiko_argument_map.items(): + for k in netmiko_argument_map: try: self.netmiko_optional_args[k] = optional_args[k] except KeyError: pass - self.global_delay_factor = optional_args.get('global_delay_factor', 1) - self.port = optional_args.get('port', 22) + self.global_delay_factor = optional_args.get("global_delay_factor", 1) + self.port = optional_args.get("port", 22) def open(self): - self.device = ConnectHandler(device_type='vyos', - host=self.hostname, - username=self.username, - password=self.password, - **self.netmiko_optional_args) + self.device = ConnectHandler( + device_type="vyos", + host=self.hostname, + username=self.username, + password=self.password, + **self.netmiko_optional_args, + ) try: self._scp_client = SCPConn(self.device) @@ -111,9 +114,7 @@ class VyOSDriver(NetworkDriver): def is_alive(self): """Returns a flag with the state of the SSH connection.""" - return { - 'is_alive': self.device.remote_conn.transport.is_active() - } + return {"is_alive": self.device.remote_conn.transport.is_active()} def load_replace_candidate(self, filename=None, config=None): """ @@ -123,38 +124,36 @@ class VyOSDriver(NetworkDriver): support a replace using a configuration string. """ if not filename and not config: - raise ReplaceConfigException('filename or config param must be provided.') + raise ReplaceConfigException("filename or config param must be provided.") if filename is None: - temp_file = tempfile.NamedTemporaryFile(mode='w+') + temp_file = tempfile.NamedTemporaryFile(mode="w+") temp_file.write(config) temp_file.flush() cfg_filename = temp_file.name else: cfg_filename = filename - - - if os.path.exists(cfg_filename) is True: - self._scp_client.scp_transfer_file(cfg_filename, self._DEST_FILENAME) - self.device.send_command("cp "+self._BOOT_FILENAME+" "+self._BACKUP_FILENAME) - output_loadcmd = self.device.send_config_set(['load '+self._DEST_FILENAME]) - match_loaded = re.findall("Load complete.", output_loadcmd) - match_notchanged = re.findall("No configuration changes to commit", output_loadcmd) - match_failed = re.findall("Failed to parse specified config file", output_loadcmd) - - if match_failed: - raise ReplaceConfigException("Failed replace config: " - + output_loadcmd) - - if not match_loaded: - if not match_notchanged: - raise ReplaceConfigException("Failed replace config: " - + output_loadcmd) - - else: + if os.path.exists(cfg_filename) is not True: raise ReplaceConfigException("config file is not found") + self._scp_client.scp_transfer_file(cfg_filename, self._DEST_FILENAME) + self.device.send_command( + f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}" + ) + output_loadcmd = self.device.send_config_set( + [f"load {self._DEST_FILENAME}"] + ) + match_loaded = re.findall("Load complete.", output_loadcmd) + match_notchanged = re.findall( + "No configuration changes to commit", output_loadcmd + ) + if match_failed := re.findall( + "Failed to parse specified config file", output_loadcmd + ): + raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") + if not match_loaded and not match_notchanged: + raise ReplaceConfigException(f"Failed replace config: {output_loadcmd}") def load_merge_candidate(self, filename=None, config=None): """ @@ -162,46 +161,42 @@ class VyOSDriver(NetworkDriver): """ if not filename and not config: - raise MergeConfigException('filename or config param must be provided.') + raise MergeConfigException("filename or config param must be provided.") if filename is None: - temp_file = tempfile.NamedTemporaryFile(mode='w+') + temp_file = tempfile.NamedTemporaryFile(mode="w+") temp_file.write(config) temp_file.flush() cfg_filename = temp_file.name else: cfg_filename = filename - - if os.path.exists(cfg_filename) is True: - with open(cfg_filename) as f: - self.device.send_command("cp "+self._BOOT_FILENAME+" " - + self._BACKUP_FILENAME) - self._new_config = f.read() - cfg = [x for x in self._new_config.split("\n") if x] - output_loadcmd = self.device.send_config_set(cfg) - match_setfailed = re.findall("Delete failed", output_loadcmd) - match_delfailed = re.findall("Set failed", output_loadcmd) - - if match_setfailed or match_delfailed: - raise MergeConfigException("Failed merge config: " - + output_loadcmd) - else: + if os.path.exists(cfg_filename) is not True: raise MergeConfigException("config file is not found") + with open(cfg_filename) as f: + self.device.send_command( + f"cp {self._BOOT_FILENAME} {self._BACKUP_FILENAME}" + ) + self._new_config = f.read() + cfg = [x for x in self._new_config.split("\n") if x] + output_loadcmd = self.device.send_config_set(cfg) + match_setfailed = re.findall("Delete failed", output_loadcmd) + match_delfailed = re.findall("Set failed", output_loadcmd) + if match_setfailed or match_delfailed: + raise MergeConfigException(f"Failed merge config: {output_loadcmd}") def discard_config(self): self.device.exit_config_mode() def compare_config(self): - output_compare = self.device.send_config_set(['compare']) - match = re.findall("No changes between working and active configurations", - output_compare) - if match: + output_compare = self.device.send_config_set(["compare"]) + if match := re.findall( + "No changes between working and active configurations", output_compare + ): return "" else: - diff = ''.join(output_compare.splitlines(True)[1:-1]) - return diff + return "".join(output_compare.splitlines(True)[1:-1]) def commit_config(self, message=""): if message: @@ -211,10 +206,10 @@ class VyOSDriver(NetworkDriver): try: self.device.commit() - except ValueError: - raise CommitError("Failed to commit config on the device") + except ValueError as e: + raise CommitError("Failed to commit config on the device") from e - self.device.send_config_set(['save']) + self.device.send_config_set(["save"]) self.device.exit_config_mode() def rollback(self): @@ -223,13 +218,13 @@ class VyOSDriver(NetworkDriver): if filename is None: filename = self._BACKUP_FILENAME - output_loadcmd = self.device.send_config_set(['load '+filename]) - match = re.findall("Load complete.", output_loadcmd) - if not match: - raise ReplaceConfigException("Failed rollback config: " - + output_loadcmd) + output_loadcmd = self.device.send_config_set([f"load {filename}"]) + if match := re.findall("Load complete.", output_loadcmd): + self.device.send_config_set(["commit", "save"]) else: - self.device.send_config_set(['commit', 'save']) + raise ReplaceConfigException( + f"Failed rollback config: {output_loadcmd}" + ) def get_environment(self): """ @@ -238,7 +233,7 @@ class VyOSDriver(NetworkDriver): r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 0 61404 139624 139360 0 0 0 0 9 14 0 0 100 0 """ - output_cpu_list = list() + output_cpu_list = [] output_cpu = self.device.send_command("vmstat") output_cpu = str(output_cpu) output_cpu_list = output_cpu.split("\n") @@ -259,39 +254,25 @@ class VyOSDriver(NetworkDriver): output_ram = self.device.send_command("free").split("\n")[1] available_ram, used_ram = output_ram.split()[1:3] - environment = { - "fans": { - "invalid": { - "status": False - } - }, - "temperature": { - "invalid": { - "temperature": 0.0, - "is_alert": False, - "is_critical": False - } - }, - "power": { - "invalid": { - "status": True, - "capacity": 0.0, - "output": 0.0 - } - }, - "cpu": { - "0": { - "%usage": float(cpu) + return { + "fans": {"invalid": {"status": False}}, + "temperature": { + "invalid": { + "temperature": 0.0, + "is_alert": False, + "is_critical": False, + } + }, + "power": {"invalid": {"status": True, "capacity": 0.0, "output": 0.0}}, + "cpu": { + "0": {"%usage": float(cpu)}, + }, + "memory": { + "available_ram": int(available_ram), + "used_ram": int(used_ram), }, - }, - "memory": { - "available_ram": int(available_ram), - "used_ram": int(used_ram) - } } - return environment - def get_interfaces(self): """ "show interfaces" output example: @@ -312,15 +293,17 @@ class VyOSDriver(NetworkDriver): # 'match' example: # [("br0", "u", "D"), ("eth0", "u", "u"), ("eth1", "u", "u")...] - iface_state = {iface_name: {"State": state, "Link": link} for iface_name, - state, link in match} + iface_state = { + iface_name: {"State": state, "Link": link} + for iface_name, state, link in match + } output_conf = self.device.send_command("show configuration") # Convert the configuration to dictionary config = vyattaconfparser.parse_conf(output_conf) - iface_dict = dict() + iface_dict = {} for iface_type in config["interfaces"]: @@ -334,20 +317,18 @@ class VyOSDriver(NetworkDriver): speed = 0 hw_id = details.get("hw-id", "00:00:00:00:00:00") - is_up = (iface_state[iface_name]["Link"] == "u") - is_enabled = (iface_state[iface_name]["State"] == "u") + is_up = iface_state[iface_name]["Link"] == "u" + is_enabled = iface_state[iface_name]["State"] == "u" - iface_dict.update({ - iface_name: { - "is_up": bool(is_up), - "is_enabled": bool(is_enabled), + iface_dict[iface_name] = { + "is_up": is_up, + "is_enabled": is_enabled, "description": description, "last_flapped": float(-1), "mtu": -1, "speed": int(speed), "mac_address": hw_id, - } - }) + } return iface_dict @@ -375,24 +356,20 @@ class VyOSDriver(NetworkDriver): # Skip the header line output = output[1:-1] - arp_table = list() + arp_table = [] for line in output: line = line.split() # 'line' example: # ["10.129.2.254", "ether", "00:50:56:97:af:b1", "C", "eth0"] # [u'10.0.12.33', u'(incomplete)', u'eth1'] - if "incomplete" in line[1]: - macaddr = "00:00:00:00:00:00" - else: - macaddr = line[2] - + macaddr = "00:00:00:00:00:00" if "incomplete" in line[1] else line[2] arp_table.append( { - 'interface': line[-1], - 'mac': macaddr, - 'ip': line[0], - 'age': 0.0, + "interface": line[-1], + "mac": macaddr, + "ip": line[0], + "age": 0.0, } ) @@ -410,48 +387,58 @@ class VyOSDriver(NetworkDriver): output = self.device.send_command("ntpq -np") output = output.split("\n")[2:] - ntp_stats = list() + ntp_stats = [] for ntp_info in output: if len(ntp_info) > 0: - remote, refid, st, t, when, hostpoll, reachability, delay, offset, \ - jitter = ntp_info.split() + ( + remote, + refid, + st, + t, + when, + hostpoll, + reachability, + delay, + offset, + jitter, + ) = ntp_info.split() # 'remote' contains '*' if the machine synchronized with NTP server synchronized = "*" in remote match = re.search(r"(\d+\.\d+\.\d+\.\d+)", remote) - ip = match.group(1) + ip = match[1] - when = when if when != '-' else 0 + when = when if when != "-" else 0 - ntp_stats.append({ - "remote": ip, - "referenceid": refid, - "synchronized": bool(synchronized), - "stratum": int(st), - "type": t, - "when": when, - "hostpoll": int(hostpoll), - "reachability": int(reachability), - "delay": float(delay), - "offset": float(offset), - "jitter": float(jitter), - }) + ntp_stats.append( + { + "remote": ip, + "referenceid": refid, + "synchronized": synchronized, + "stratum": int(st), + "type": t, + "when": when, + "hostpoll": int(hostpoll), + "reachability": int(reachability), + "delay": float(delay), + "offset": float(offset), + "jitter": float(jitter), + } + ) return ntp_stats def get_ntp_peers(self): output = self.device.send_command("ntpq -np") output_peers = output.split("\n")[2:] - ntp_peers = dict() + ntp_peers = {} for line in output_peers: if len(line) > 0: match = re.search(r"(\d+\.\d+\.\d+\.\d+)\s+", line) - ntp_peers.update({ - match.group(1): {} - }) + ntp_peers[match[1]] = {} return ntp_peers @@ -472,139 +459,248 @@ class VyOSDriver(NetworkDriver): """ output = self.device.send_command("show ip bgp summary") - output = output.split("\n") + logger.warning("BGP summary output: %s" % output) + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join(current_dir, "templates", "bgp_sum.template") - match = re.search(r".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", - output[0]) - if not match: - return {} - router_id = match.group(1) - local_as = int(match.group(2)) + # Assuming you've got a TextFSM template ready to parse the `bgp_sum` output + with open(template_path) as template_file: + logger.warning("Checking template file") + fsm = textfsm.TextFSM(template_file) + header = fsm.header + result = fsm.ParseText(output) + + logger.warning("GOT RESULT:") + logger.warning(result) - bgp_neighbor_data = dict() - bgp_neighbor_data["global"] = dict() - bgp_neighbor_data["global"]["router_id"] = router_id - bgp_neighbor_data["global"]["peers"] = {} + bgp_neighbor_data = {"global": {"router_id": "", "peers": {}}} + #return bgp_neighbor_data + + try: + bgp_neighbor_data["global"]["router_id"] = result[0][ + header.index("BGP_ROUTER_ID") + ] + except Exception as e: + logger.error("Error parsing BGP router ID: %s" % e) + logger.error(result) + pass - # delete the header and empty element - bgp_info = [i.strip() for i in output[6:-2] if i] + for neighbor in result: + logger.warning("Parsing BGP neighbor %s" % neighbor) + peer_id = neighbor[header.index("NEIGHBOR")] + bgp_neighbor_data["global"]["peers"][peer_id] = [] - for i in bgp_info: - if len(i) > 0: - peer_id, bgp_version, remote_as, msg_rcvd, msg_sent, table_version, \ - in_queue, out_queue, up_time, state_prefix = i.split() + peer_dict = { + "description": str(neighbor[header.index("DESCRIPTION")]), + "is_enabled": "Admin" not in neighbor[header.index("PREFIX_SENT")], + "local_as": int(neighbor[header.index("LOCAL_AS")]), + "is_up": "Active" + not in neighbor[header.index("STATE_PREFIX_RECEIVED")], + "remote_id": neighbor[header.index("NEIGHBOR")], + "remote_address": neighbor[header.index("NEIGHBOR")], + "uptime": int( + self._bgp_time_conversion(neighbor[header.index("UP_TIME")]) + ), + "remote_as": int(neighbor[header.index("NEIGHBOR_AS")]), + "prefixes_sent": int(neighbor[header.index("PREFIX_SENT")]), + } - is_enabled = "(Admin)" not in state_prefix - - received_prefixes = None - - try: - state_prefix = int(state_prefix) - received_prefixes = int(state_prefix) - is_up = True - except ValueError: - state_prefix = -1 - received_prefixes = -1 - is_up = False - - if bgp_version == "4": - address_family = "ipv4" - elif bgp_version == "6": - address_family = "ipv6" - else: - raise ValueError("BGP neighbor parsing failed") - - """ - 'show ip bgp neighbors 192.168.1.1' output example: - BGP neighbor is 192.168.1.1, remote AS 64519, local AS 64520, external link - BGP version 4, remote router ID 192.168.1.1 - For address family: IPv4 Unicast - ~~~ - Community attribute sent to this neighbor(both) - 1 accepted prefixes - ~~~ - """ - bgp_detail = self.device.send_command("show ip bgp neighbors %s" % peer_id) - - match_rid = re.search(r"remote router ID (\d+\.\d+\.\d+\.\d+).*", bgp_detail) - remote_rid = match_rid.group(1) - - match_prefix_accepted = re.search(r"(\d+) accepted prefixes", bgp_detail) - accepted_prefixes = match_prefix_accepted.group(1) - - bgp_neighbor_data["global"]["peers"].setdefault(peer_id, {}) - peer_dict = { - "description": "", - "is_enabled": bool(is_enabled), - "local_as": int(local_as), - "is_up": bool(is_up), - "remote_id": remote_rid, - "uptime": int(self._bgp_time_conversion(up_time)), - "remote_as": int(remote_as) - } - - af_dict = dict() - af_dict[address_family] = { - "sent_prefixes": int(-1), - "accepted_prefixes": int(accepted_prefixes), - "received_prefixes": int(received_prefixes) - } - - peer_dict["address_family"] = af_dict - bgp_neighbor_data["global"]["peers"][peer_id] = peer_dict + bgp_neighbor_data["global"]["peers"][peer_id] = peer_dict return bgp_neighbor_data - def _bgp_time_conversion(self, bgp_uptime): - # uptime_letters = set(["y", "w", "h", "d"]) + def get_bgp_neighbors_detail(self, neighbor_address=""): + def safe_int(value, default=0): + try: + return int(value) if value and value.isdigit() else default + except ValueError: + return default + + neighbors = self.get_bgp_neighbors() + logging.warning(neighbors) + bgp_neighbor_data = {"global": {}} + + for neighbor_id, peer_dict in neighbors["global"]["peers"].items(): + if neighbor_address and neighbor_address != neighbor_id: + logger.warning("Neighbor address has been specificly set") + continue + logger.warning("Parsing BGP neighbor details %s" % neighbor_id) + + output = self.device.send_command(f"show ip bgp neighbor {neighbor_id}") + + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join( + current_dir, "templates", "bgp_details.template" + ) + + with open(template_path) as template_file: + fsm = textfsm.TextFSM(template_file) + result = fsm.ParseText(output) + + if not result: + continue + + neighbors_dicts = [ + dict(zip(fsm.header, neighbor)) for neighbor in result + ] + + for neighbor in neighbors_dicts: + logging.warning(f"Peer ID: {neighbor_id}, Is Enabled: {peer_dict['is_enabled']}") + #logger.warning("Parsing BGP neighbor details %s" % neighbor) + remote_as = neighbor["REMOTE_AS"] + logger.warning("Remote AS: %s" % remote_as) + logger.warning("BGP State: %s" % neighbor["BGP_STATE"]) + status = "disabled" if neighbor["BGP_STATE"].lower() == "idle" else "enabled", + logger.warning("Status: %s" % status) + + peer_dict = { + "up": neighbor["BGP_STATE"].lower() == "established", + "is_enabled": neighbor["BGP_STATE"].lower() == "established", + "status": "disabled" if neighbor["BGP_STATE"].lower() == "idle" else "enabled", + "local_as": int(neighbor["LOCAL_AS"]), + "remote_as": int(neighbor["REMOTE_AS"]), + "router_id": neighbor["LOCAL_ROUTER_ID"], + "local_address": neighbor[ + "LOCAL_ROUTER_ID" + ], # Adjusted from LOCAL_ROUTER_ID based on context + "routing_table": f"IPv{neighbor['BGP_VERSION']} Unicast", # Constructed value + "local_address_configured": bool(neighbor["LOCAL_ROUTER_ID"]), + "local_port": ( + int(neighbor["LOCAL_PORT"]) + if neighbor["LOCAL_PORT"].isdigit() + else None + ), + "remote_address": neighbor["NEIGHBOR"], + "remote_port": neighbor["FOREIGN_PORT"], + "multipath": neighbor.get( + "DYNAMIC_CAPABILITY", "no" + ), # Assuming DYNAMIC_CAPABILITY indicates multipath + "remove_private_as": ( + "yes" + if neighbor.get("REMOVE_PRIVATE_AS", "no") != "no" + else "no" + ), # Placeholder for actual value + "input_messages": sum( + int(neighbor["MESSAGE_STATISTICS_RECEIVED"][i]) + for i in range(len(neighbor["MESSAGE_STATISTICS_TYPE"])) + if neighbor["MESSAGE_STATISTICS_TYPE"][i] + in ["Updates", "Keepalives"] + ), + "output_messages": sum( + int(neighbor["MESSAGE_STATISTICS_SENT"][i]) + for i in range(len(neighbor["MESSAGE_STATISTICS_TYPE"])) + if neighbor["MESSAGE_STATISTICS_TYPE"][i] + in ["Updates", "Keepalives"] + ), + "input_updates": safe_int( + neighbor.get("RECEIVED_PREFIXES_IPV4") + ) + + safe_int(neighbor.get("RECEIVED_PREFIXES_IPV6")), + "output_updates": safe_int( + neighbor.get("ADVERTISED_PREFIX_COUNT") + ), + "connection_state": neighbor["BGP_STATE"].replace(",", "").lower(), + "bgp_state": neighbor["BGP_STATE"].lower(), + "previous_connection_state": neighbor.get( + "LAST_RESET_REASON", "unknown" + ), + "last_event": neighbor.get( + "LAST_EVENT", "Not Available" + ), # Assuming LAST_EVENT is available + "suppress_4byte_as": neighbor.get( + "FOUR_BYTE_AS_CAPABILITY", "Not Configured" + ), + "local_as_prepend": neighbor.get( + "LOCAL_AS_PREPEND", "Not Configured" + ), # Assuming LOCAL_AS_PREPEND is available + "holdtime": int(neighbor["HOLD_TIME"]), + "configured_holdtime": int(neighbor["CONFIGURED_HOLD_TIME"]), + "keepalive": int(neighbor["KEEPALIVE_INTERVAL"]), + "configured_keepalive": int( + neighbor["CONFIGURED_KEEPALIVE_INTERVAL"] + ), + "active_prefix_count": int( + neighbor.get("ACTIVE_PREFIX_COUNT", 0) + ), # Assuming ACTIVE_PREFIX_COUNT is available + "accepted_prefix_count": int( + neighbor.get("ACCEPTED_PREFIX_COUNT", 0) + ), # Assuming ACCEPTED_PREFIX_COUNT is available + "suppressed_prefix_count": int( + neighbor.get("SUPPRESSED_PREFIX_COUNT", 0) + ), # Assuming SUPPRESSED_PREFIX_COUNT is available + "advertised_prefix_count": int( + peer_dict['prefixes_sent'] + ), + "received_prefix_count": safe_int( + neighbor.get("RECEIVED_PREFIXES_IPV4", 0) + ) + + safe_int(neighbor.get("RECEIVED_PREFIXES_IPV6", 0)), + "flap_count": safe_int( + neighbor.get("FLAP_COUNT", 0) + ), # Assuming FLAP_COUNT is available + } + + + + bgp_neighbor_data["global"].setdefault(neighbor["NEIGHBOR"], []).append( + peer_dict + ) + + logger.warning("Returning FULL DICT: %s" % bgp_neighbor_data) + return bgp_neighbor_data + + def _bgp_time_conversion(self, bgp_uptime): if "never" in bgp_uptime: return -1 + if "y" in bgp_uptime: + match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) + return ( + int(match[1]) * self._YEAR_SECONDS + + int(match[3]) * self._WEEK_SECONDS + + int(match[5]) * self._DAY_SECONDS + ) + elif "w" in bgp_uptime: + match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) + return ( + int(match[1]) * self._WEEK_SECONDS + + int(match[3]) * self._DAY_SECONDS + ) + int(match[5]) * self._HOUR_SECONDS + elif "d" in bgp_uptime: + match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) + return ( + (int(match.group(1)) * self._DAY_SECONDS) + + (int(match.group(3)) * self._HOUR_SECONDS) + + (int(match.group(5)) * self._MINUTE_SECONDS) + ) else: - if "y" in bgp_uptime: - match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) - uptime = ((int(match.group(1)) * self._YEAR_SECONDS) + - (int(match.group(3)) * self._WEEK_SECONDS) + - (int(match.group(5)) * self._DAY_SECONDS)) - return uptime - elif "w" in bgp_uptime: - match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) - uptime = ((int(match.group(1)) * self._WEEK_SECONDS) + - (int(match.group(3)) * self._DAY_SECONDS) + - (int(match.group(5)) * self._HOUR_SECONDS)) - return uptime - elif "d" in bgp_uptime: - match = re.search(r"(\d+)(\w)(\d+)(\w)(\d+)(\w)", bgp_uptime) - uptime = ((int(match.group(1)) * self._DAY_SECONDS) + - (int(match.group(3)) * self._HOUR_SECONDS) + - (int(match.group(5)) * self._MINUTE_SECONDS)) - return uptime - else: - hours, minutes, seconds = map(int, bgp_uptime.split(":")) - uptime = ((hours * self._HOUR_SECONDS) + - (minutes * self._MINUTE_SECONDS) + seconds) - return uptime + hours, minutes, seconds = map(int, bgp_uptime.split(":")) + return ( + (hours * self._HOUR_SECONDS) + + (minutes * self._MINUTE_SECONDS) + + seconds + ) def get_lldp_neighbors(self): # Multiple neighbors per port are not implemented # The show lldp neighbors commands lists port descriptions, not IDs output = self.device.send_command("show lldp neighbors detail") - pattern = r'''(?s)Interface: +(?P\S+), [^\n]+ + pattern = r"""(?s)Interface: +(?P\S+), [^\n]+ .+? +SysName: +(?P\S+) .+? - +PortID: +ifname (?P\S+)''' + +PortID: +ifname (?P\S+)""" def _get_interface(match): return [ { - 'hostname': match.group('hostname'), - 'port': match.group('port'), + "hostname": match.group("hostname"), + "port": match.group("port"), } ] return { - match.group('interface'): _get_interface(match) + match.group("interface"): _get_interface(match) for match in re.finditer(pattern, output) } @@ -627,11 +723,9 @@ class VyOSDriver(NetworkDriver): interfaces = re.findall(r"(\S+): <.*", output) # count = re.findall("(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+", output) count = re.findall(r"(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)", output) - counters = dict() + counters = {} - j = 0 - - for i in count: + for j, i in enumerate(count): if j % 2 == 0: rx_errors = i[2] rx_discards = i[3] @@ -640,24 +734,20 @@ class VyOSDriver(NetworkDriver): rx_multicast_packets = i[5] rx_broadcast_packets = -1 else: - counters.update({ - interfaces[j // 2]: { - "tx_errors": int(i[2]), - "tx_discards": int(i[3]), - "tx_octets": int(i[0]), - "tx_unicast_packets": int(i[1]), - "tx_multicast_packets": int(-1), - "tx_broadcast_packets": int(-1), - "rx_errors": int(rx_errors), - "rx_discards": int(rx_discards), - "rx_octets": int(rx_octets), - "rx_unicast_packets": int(rx_unicast_packets), - "rx_multicast_packets": int(rx_multicast_packets), - "rx_broadcast_packets": int(rx_broadcast_packets) - } - }) - j += 1 - + counters[interfaces[j // 2]] = { + "tx_errors": int(i[2]), + "tx_discards": int(i[3]), + "tx_octets": int(i[0]), + "tx_unicast_packets": int(i[1]), + "tx_multicast_packets": -1, + "tx_broadcast_packets": -1, + "rx_errors": int(rx_errors), + "rx_discards": int(rx_discards), + "rx_octets": int(rx_octets), + "rx_unicast_packets": int(rx_unicast_packets), + "rx_multicast_packets": int(rx_multicast_packets), + "rx_broadcast_packets": int(rx_broadcast_packets), + } return counters def get_snmp_information(self): @@ -667,23 +757,25 @@ class VyOSDriver(NetworkDriver): # convert the configuration to dictionary config = vyattaconfparser.parse_conf(output) - snmp = dict() - snmp["community"] = dict() - + snmp = {"community": {}} try: for i in config["service"]["snmp"]["community"]: - snmp["community"].update({ - i: { - "acl": "", - "mode": config["service"]["snmp"]["community"][i]["authorization"], + snmp["community"].update( + { + i: { + "acl": "", + "mode": config["service"]["snmp"]["community"][i][ + "authorization" + ], + } } - }) + ) - snmp.update({ - "chassis_id": "", - "contact": config["service"]["snmp"]["contact"], - "location": config["service"]["snmp"]["location"], - }) + snmp |= { + "chassis_id": "", + "contact": config["service"]["snmp"]["contact"], + "location": config["service"]["snmp"]["location"], + } return snmp except KeyError: @@ -698,7 +790,7 @@ class VyOSDriver(NetworkDriver): ver_str = [line for line in output if "Version" in line][0] version = self.parse_version(ver_str) - above_1_1 = False if version.startswith('1.0') or version.startswith('1.1') else True + above_1_1 = not version.startswith("1.0") and not version.startswith("1.1") if above_1_1: sn_str = [line for line in output if "Hardware S/N" in line][0] hwmodel_str = [line for line in output if "Hardware model" in line][0] @@ -722,28 +814,27 @@ class VyOSDriver(NetworkDriver): else: fqdn = "" - iface_list = list() + iface_list = [] for iface_type in config["interfaces"]: for iface_name in config["interfaces"][iface_type]: iface_list.append(iface_name) facts = { - "uptime": int(uptime), - "vendor": "VyOS", - "os_version": version, - "serial_number": snumber, - "model": hwmodel, - "hostname": hostname, - "fqdn": fqdn, - "interface_list": iface_list, + "uptime": int(uptime), + "vendor": "VyOS", + "os_version": version, + "serial_number": snumber, + "model": hwmodel, + "hostname": hostname, + "fqdn": fqdn, + "interface_list": iface_list, } return facts @staticmethod def parse_version(ver_str): - version = ver_str.split()[-1] - return version + return ver_str.split()[-1] @staticmethod def parse_snumber(sn_str): @@ -765,7 +856,7 @@ class VyOSDriver(NetworkDriver): else: ifaces = [x for x in output[3:-1] if "-" not in x] - ifaces_ip = dict() + ifaces_ip = {} for iface in ifaces: iface = iface.split() @@ -776,14 +867,14 @@ class VyOSDriver(NetworkDriver): # Delete the "Interface" column iface = iface[1:-1] # Key initialization - ifaces_ip[iface_name] = dict() + ifaces_ip[iface_name] = {} ip_addr, mask = iface[0].split("/") ip_ver = self._get_ip_version(ip_addr) # Key initialization if ip_ver not in ifaces_ip[iface_name]: - ifaces_ip[iface_name][ip_ver] = dict() + ifaces_ip[iface_name][ip_ver] = {} ifaces_ip[iface_name][ip_ver][ip_addr] = {"prefix_length": int(mask)} @@ -802,12 +893,12 @@ class VyOSDriver(NetworkDriver): user_conf = [x.split() for x in output if "login user" in x] # Collect all users' name - user_name = list(set([x[4] for x in user_conf])) + user_name = list({x[4] for x in user_conf}) - user_auth = dict() + user_auth = {} for user in user_name: - sshkeys = list() + sshkeys = [] # extract the configuration which relates to 'user' for line in [x for x in user_conf if user in x]: @@ -816,56 +907,41 @@ class VyOSDriver(NetworkDriver): if line[6] == "encrypted-password": password = line[7].strip("'") - # set system login user alice level 'admin' elif line[5] == "level": - if line[6].strip("'") == "admin": - level = 15 - else: - level = 0 - - # "set system login user alice authentication public-keys - # alice@example.com key 'ABC'" + level = 15 if line[6].strip("'") == "admin" else 0 elif len(line) == 10 and line[8] == "key": sshkeys.append(line[9].strip("'")) - user_auth.update({ - user: { - "level": level, - "password": password, - "sshkeys": sshkeys - } - }) + user_auth[user] = {"level": level, "password": password, "sshkeys": sshkeys} return user_auth - def ping(self, - destination, - source=C.PING_SOURCE, - ttl=C.PING_TTL, - timeout=C.PING_TIMEOUT, - size=C.PING_SIZE, - count=C.PING_COUNT, - vrf=C.PING_VRF): + def ping( + self, + destination, + source=C.PING_SOURCE, + ttl=C.PING_TTL, + timeout=C.PING_TIMEOUT, + size=C.PING_SIZE, + count=C.PING_COUNT, + vrf=C.PING_VRF, + ): # does not support multiple destination yet deadline = timeout * count - command = "ping %s " % destination + command = f"ping {destination} " command += "ttl %d " % ttl command += "deadline %d " % deadline command += "size %d " % size command += "count %d " % count if source != "": - command += "interface %s " % source + command += f"interface {source} " - ping_result = dict() + ping_result = {} output_ping = self.device.send_command(command) - if "Unknown host" in output_ping: - err = "Unknown host" - else: - err = "" - + err = "Unknown host" if "Unknown host" in output_ping else "" if err: ping_result["error"] = err else: @@ -874,11 +950,9 @@ class VyOSDriver(NetworkDriver): # 'loss,', 'time', '3997ms'] packet_info = output_ping.split("\n") - if len(packet_info[-1]) > 0: - packet_info = packet_info[-2] - else: - packet_info = packet_info[-3] - + packet_info = ( + packet_info[-2] if len(packet_info[-1]) > 0 else packet_info[-3] + ) packet_info = [x.strip() for x in packet_info.split()] sent = int(packet_info[0]) @@ -889,25 +963,21 @@ class VyOSDriver(NetworkDriver): # ["0.307/0.396/0.480/0.061"] rtt_info = output_ping.split("\n") - if len(rtt_info[-1]) > 0: - rtt_info = rtt_info[-1] - else: - rtt_info = rtt_info[-2] - + rtt_info = rtt_info[-1] if len(rtt_info[-1]) > 0 else rtt_info[-2] match = re.search(r"([\d\.]+)/([\d\.]+)/([\d\.]+)/([\d\.]+)", rtt_info) if match is not None: - rtt_min = float(match.group(1)) - rtt_avg = float(match.group(2)) - rtt_max = float(match.group(3)) - rtt_stddev = float(match.group(4)) + rtt_min = float(match[1]) + rtt_avg = float(match[2]) + rtt_max = float(match[3]) + rtt_stddev = float(match[4]) else: rtt_min = None rtt_avg = None rtt_max = None rtt_stddev = None - ping_result["success"] = dict() + ping_result["success"] = {} ping_result["success"] = { "probes_sent": sent, "packet_loss": lost, @@ -915,7 +985,7 @@ class VyOSDriver(NetworkDriver): "rtt_max": rtt_max, "rtt_avg": rtt_avg, "rtt_stddev": rtt_stddev, - "results": [{"ip_address": destination, "rtt": rtt_avg}] + "results": [{"ip_address": destination, "rtt": rtt_avg}], } return ping_result @@ -933,20 +1003,20 @@ class VyOSDriver(NetworkDriver): - startup(string) - Representation of the native startup configuration. """ if retrieve not in ["running", "candidate", "startup", "all"]: - raise Exception("ERROR: Not a valid option to retrieve.\nPlease select from 'running', 'candidate', " - "'startup', or 'all'") + raise Exception( + "ERROR: Not a valid option to retrieve.\nPlease select from 'running', 'candidate', " + "'startup', or 'all'" + ) else: - config_dict = { - "running": "", - "startup": "", - "candidate": "" - } + config_dict = {"running": "", "startup": "", "candidate": ""} if retrieve in ["running", "all"]: - config_dict['running'] = self._get_running_config(sanitized) + config_dict["running"] = self._get_running_config(sanitized) if retrieve in ["startup", "all"]: - config_dict['startup'] = self.device.send_command(f"cat {self._BOOT_FILENAME}") + config_dict["startup"] = self.device.send_command( + f"cat {self._BOOT_FILENAME}" + ) if retrieve in ["candidate", "all"]: - config_dict['candidate'] = self._new_config or "" + config_dict["candidate"] = self._new_config or "" return config_dict @@ -955,6 +1025,6 @@ class VyOSDriver(NetworkDriver): return self.device.send_command("show configuration") self.device.config_mode() config = self.device.send_command("show") - config = config[:config.rfind('\n')] + config = config[: config.rfind("\n")] self.device.exit_config_mode() return config From 72f52ea38771da390b3841fb423042089bcc1a46 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Wed, 25 Sep 2024 15:44:37 +0200 Subject: [PATCH 08/15] Update vyos.py Debugging --- napalm_vyos/vyos.py | 85 +++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index eeb469f..b20445c 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -531,92 +531,95 @@ class VyOSDriver(NetworkDriver): dict(zip(fsm.header, neighbor)) for neighbor in result ] - for neighbor in neighbors_dicts: + logger.error(f"Size of neighbors_dicts is " + len(neighbors_dicts)) - remote_as = neighbor["REMOTE_AS"] + for neighbor_detail in neighbors_dicts: + + remote_as = neighbor_detail["REMOTE_AS"] + logger.error(f"Parsing AS {remote_as} for neighbor {neighbor}") peer_dict = { - "up": neighbor["BGP_STATE"].lower() == "established", - "local_as": int(neighbor["LOCAL_AS"]), - "remote_as": int(neighbor["REMOTE_AS"]), - "router_id": neighbor["LOCAL_ROUTER_ID"], - "local_address": neighbor[ + "up": neighbor_detail["BGP_STATE"].lower() == "established", + "local_as": int(neighbor_detail["LOCAL_AS"]), + "remote_as": int(neighbor_detail["REMOTE_AS"]), + "router_id": neighbor_detail["LOCAL_ROUTER_ID"], + "local_address": neighbor_detail[ "LOCAL_ROUTER_ID" ], # Adjusted from LOCAL_ROUTER_ID based on context - "routing_table": f"IPv{neighbor['BGP_VERSION']} Unicast", # Constructed value - "local_address_configured": bool(neighbor["LOCAL_ROUTER_ID"]), + "routing_table": f"IPv{neighbor_detail['BGP_VERSION']} Unicast", # Constructed value + "local_address_configured": bool(neighbor_detail["LOCAL_ROUTER_ID"]), "local_port": ( - int(neighbor["LOCAL_PORT"]) - if neighbor["LOCAL_PORT"].isdigit() + int(neighbor_detail["LOCAL_PORT"]) + if neighbor_detail["LOCAL_PORT"].isdigit() else None ), - "remote_address": neighbor["REMOTE_ROUTER_ID"], - "remote_port": neighbor["FOREIGN_PORT"], - "multipath": neighbor.get( + "remote_address": neighbor_detail["REMOTE_ROUTER_ID"], + "remote_port": neighbor_detail["FOREIGN_PORT"], + "multipath": neighbor_detail.get( "DYNAMIC_CAPABILITY", "no" ), # Assuming DYNAMIC_CAPABILITY indicates multipath "remove_private_as": ( "yes" - if neighbor.get("REMOVE_PRIVATE_AS", "no") != "no" + if neighbor_detail.get("REMOVE_PRIVATE_AS", "no") != "no" else "no" ), # Placeholder for actual value "input_messages": sum( - int(neighbor["MESSAGE_STATISTICS_RECEIVED"][i]) - for i in range(len(neighbor["MESSAGE_STATISTICS_TYPE"])) - if neighbor["MESSAGE_STATISTICS_TYPE"][i] + int(neighbor_detail["MESSAGE_STATISTICS_RECEIVED"][i]) + for i in range(len(neighbor_detail["MESSAGE_STATISTICS_TYPE"])) + if neighbor_detail["MESSAGE_STATISTICS_TYPE"][i] in ["Updates", "Keepalives"] ), "output_messages": sum( - int(neighbor["MESSAGE_STATISTICS_SENT"][i]) - for i in range(len(neighbor["MESSAGE_STATISTICS_TYPE"])) - if neighbor["MESSAGE_STATISTICS_TYPE"][i] + int(neighbor_detail["MESSAGE_STATISTICS_SENT"][i]) + for i in range(len(neighbor_detail["MESSAGE_STATISTICS_TYPE"])) + if neighbor_detail["MESSAGE_STATISTICS_TYPE"][i] in ["Updates", "Keepalives"] ), "input_updates": safe_int( - neighbor.get("RECEIVED_PREFIXES_IPV4") + neighbor_detail.get("RECEIVED_PREFIXES_IPV4") ) - + safe_int(neighbor.get("RECEIVED_PREFIXES_IPV6")), + + safe_int(neighbor_detail.get("RECEIVED_PREFIXES_IPV6")), "output_updates": safe_int( - neighbor.get("ADVERTISED_PREFIX_COUNT") + neighbor_detail.get("ADVERTISED_PREFIX_COUNT") ), - "connection_state": neighbor["BGP_STATE"].lower(), - "bgp_state": neighbor["BGP_STATE"].lower(), - "previous_connection_state": neighbor.get( + "connection_state": neighbor_detail["BGP_STATE"].lower(), + "bgp_state": neighbor_detail["BGP_STATE"].lower(), + "previous_connection_state": neighbor_detail.get( "LAST_RESET_REASON", "unknown" ), - "last_event": neighbor.get( + "last_event": neighbor_detail.get( "LAST_EVENT", "Not Available" ), # Assuming LAST_EVENT is available - "suppress_4byte_as": neighbor.get( + "suppress_4byte_as": neighbor_detail.get( "FOUR_BYTE_AS_CAPABILITY", "Not Configured" ), - "local_as_prepend": neighbor.get( + "local_as_prepend": neighbor_detail.get( "LOCAL_AS_PREPEND", "Not Configured" ), # Assuming LOCAL_AS_PREPEND is available - "holdtime": int(neighbor["HOLD_TIME"]), - "configured_holdtime": int(neighbor["CONFIGURED_HOLD_TIME"]), - "keepalive": int(neighbor["KEEPALIVE_INTERVAL"]), + "holdtime": int(neighbor_detail["HOLD_TIME"]), + "configured_holdtime": int(neighbor_detail["CONFIGURED_HOLD_TIME"]), + "keepalive": int(neighbor_detail["KEEPALIVE_INTERVAL"]), "configured_keepalive": int( - neighbor["CONFIGURED_KEEPALIVE_INTERVAL"] + neighbor_detail["CONFIGURED_KEEPALIVE_INTERVAL"] ), "active_prefix_count": int( - neighbor.get("ACTIVE_PREFIX_COUNT", 0) + neighbor_detail.get("ACTIVE_PREFIX_COUNT", 0) ), # Assuming ACTIVE_PREFIX_COUNT is available "accepted_prefix_count": int( - neighbor.get("ACCEPTED_PREFIX_COUNT", 0) + neighbor_detail.get("ACCEPTED_PREFIX_COUNT", 0) ), # Assuming ACCEPTED_PREFIX_COUNT is available "suppressed_prefix_count": int( - neighbor.get("SUPPRESSED_PREFIX_COUNT", 0) + neighbor_detail.get("SUPPRESSED_PREFIX_COUNT", 0) ), # Assuming SUPPRESSED_PREFIX_COUNT is available "advertised_prefix_count": int( - neighbor.get("ADVERTISED_PREFIX_COUNT", 0) + neighbor_detail.get("ADVERTISED_PREFIX_COUNT", 0) ), "received_prefix_count": safe_int( - neighbor.get("RECEIVED_PREFIXES_IPV4", 0) + neighbor_detail.get("RECEIVED_PREFIXES_IPV4", 0) ) - + safe_int(neighbor.get("RECEIVED_PREFIXES_IPV6", 0)), + + safe_int(neighbor_detail.get("RECEIVED_PREFIXES_IPV6", 0)), "flap_count": safe_int( - neighbor.get("FLAP_COUNT", 0) + neighbor_detail.get("FLAP_COUNT", 0) ), # Assuming FLAP_COUNT is available } From 0946f18ddbe1e6c52194abd9df32e4765486e287 Mon Sep 17 00:00:00 2001 From: Wieger Bontekoe Date: Thu, 26 Sep 2024 06:03:56 +0200 Subject: [PATCH 09/15] Update vyos.py --- napalm_vyos/vyos.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index b20445c..268315e 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -507,6 +507,8 @@ class VyOSDriver(NetworkDriver): except ValueError: return default + bgp_neighbor_data = {"global": {}} + neighbors = self.get_bgp_neighbors() for neighbor in neighbors["global"]["peers"]: @@ -518,8 +520,6 @@ class VyOSDriver(NetworkDriver): current_dir, "templates", "bgp_details.template" ) - bgp_neighbor_data = {"global": {}} - with open(template_path) as template_file: fsm = textfsm.TextFSM(template_file) result = fsm.ParseText(output) @@ -531,12 +531,10 @@ class VyOSDriver(NetworkDriver): dict(zip(fsm.header, neighbor)) for neighbor in result ] - logger.error(f"Size of neighbors_dicts is " + len(neighbors_dicts)) - for neighbor_detail in neighbors_dicts: remote_as = neighbor_detail["REMOTE_AS"] - logger.error(f"Parsing AS {remote_as} for neighbor {neighbor}") + logger.debug(f"Parsing AS {remote_as} for neighbor {neighbor}") peer_dict = { "up": neighbor_detail["BGP_STATE"].lower() == "established", @@ -553,7 +551,7 @@ class VyOSDriver(NetworkDriver): if neighbor_detail["LOCAL_PORT"].isdigit() else None ), - "remote_address": neighbor_detail["REMOTE_ROUTER_ID"], + "remote_address": neighbor, "remote_port": neighbor_detail["FOREIGN_PORT"], "multipath": neighbor_detail.get( "DYNAMIC_CAPABILITY", "no" @@ -582,8 +580,8 @@ class VyOSDriver(NetworkDriver): "output_updates": safe_int( neighbor_detail.get("ADVERTISED_PREFIX_COUNT") ), - "connection_state": neighbor_detail["BGP_STATE"].lower(), - "bgp_state": neighbor_detail["BGP_STATE"].lower(), + "connection_state": neighbor_detail["BGP_STATE"].lower().strip(','), + "bgp_state": neighbor_detail["BGP_STATE"].lower().strip(','), "previous_connection_state": neighbor_detail.get( "LAST_RESET_REASON", "unknown" ), @@ -626,8 +624,10 @@ class VyOSDriver(NetworkDriver): bgp_neighbor_data["global"].setdefault(int(remote_as), []).append( peer_dict ) + logger.debug("Connection state: " + neighbor_detail["BGP_STATE"].lower().strip(',')) - return bgp_neighbor_data + + return bgp_neighbor_data def _bgp_time_conversion(self, bgp_uptime): if "never" in bgp_uptime: From 90742c98b8859b1f1ea2d70eac874c07a464872d Mon Sep 17 00:00:00 2001 From: Hanarion Date: Sun, 20 Apr 2025 22:29:17 +0200 Subject: [PATCH 10/15] Update MANIFEST.in to add templates in the package --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 8981d77..5f22e23 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include requirements.txt include napalm_vyos/templates/*.j2 +include napalm_vyos/templates/*.template include napalm_vyos/utils/textfsm_templates/*.tpl From 01c02019d0004c19244573a7e77f354bebd57caa Mon Sep 17 00:00:00 2001 From: Hanarion Date: Sun, 20 Apr 2025 22:48:01 +0200 Subject: [PATCH 11/15] Fix BGP summary template for recent versions of VyOS --- napalm_vyos/templates/bgp_sum.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napalm_vyos/templates/bgp_sum.template b/napalm_vyos/templates/bgp_sum.template index 023967e..c67194a 100644 --- a/napalm_vyos/templates/bgp_sum.template +++ b/napalm_vyos/templates/bgp_sum.template @@ -14,7 +14,7 @@ Value Required PREFIX_SENT (\d+) Value Required DESCRIPTION (.+) Start - ^BGP router identifier ${BGP_ROUTER_ID}, local AS number ${LOCAL_AS} vrf-id ${VRF_ID} -> Neighbors + ^BGP router identifier ${BGP_ROUTER_ID}, local AS number ${LOCAL_AS} .* vrf-id ${VRF_ID} -> Neighbors Neighbors ^Neighbor\s+V\s+AS\s+MsgRcvd\s+MsgSent\s+TblVer\s+InQ\s+OutQ\s+Up/Down\s+State/PfxRcd\s+PfxSnt\s+Desc -> PeerLine From 4b7c9146331e3037ea72e11c668ee95e8ffa4d4d Mon Sep 17 00:00:00 2001 From: Hanarion Date: Sun, 20 Apr 2025 22:51:18 +0200 Subject: [PATCH 12/15] Allows for using the module without peering-manager --- napalm_vyos/vyos.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index 268315e..627fd62 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -24,14 +24,15 @@ import re import tempfile import textfsm import vyattaconfparser +try: + import logging + logger = logging.getLogger("peering.manager.peering") -import logging -logger = logging.getLogger("peering.manager.peering") - -from django.core.cache import cache - -cache.clear() + from django.core.cache import cache + cache.clear() +except Exception: + pass # NAPALM base import napalm.base.constants as C from napalm.base.base import NetworkDriver From 2a32bdf380cc965e13a3b89d942e23dd3184924c Mon Sep 17 00:00:00 2001 From: Hanarion Date: Sun, 20 Apr 2025 23:10:39 +0200 Subject: [PATCH 13/15] Fix IPv6 neigbour informations --- napalm_vyos/vyos.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index 268315e..22ddc1e 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -460,7 +460,7 @@ class VyOSDriver(NetworkDriver): 192.168.1.4 4 64522 0 0 0 0 0 never Active """ - output = self.device.send_command("show ip bgp summary") + output = self.device.send_command("show bgp summary") current_dir = os.path.dirname(os.path.abspath(__file__)) template_path = os.path.join(current_dir, "templates", "bgp_sum.template") @@ -513,7 +513,7 @@ class VyOSDriver(NetworkDriver): for neighbor in neighbors["global"]["peers"]: - output = self.device.send_command(f"show ip bgp neighbor {neighbor}") + output = self.device.send_command(f"show bgp neighbor {neighbor}") current_dir = os.path.dirname(os.path.abspath(__file__)) template_path = os.path.join( @@ -542,7 +542,7 @@ class VyOSDriver(NetworkDriver): "remote_as": int(neighbor_detail["REMOTE_AS"]), "router_id": neighbor_detail["LOCAL_ROUTER_ID"], "local_address": neighbor_detail[ - "LOCAL_ROUTER_ID" + "LOCAL_HOST" ], # Adjusted from LOCAL_ROUTER_ID based on context "routing_table": f"IPv{neighbor_detail['BGP_VERSION']} Unicast", # Constructed value "local_address_configured": bool(neighbor_detail["LOCAL_ROUTER_ID"]), From 56e8ddcb354e22968320ad7e973b13458d708f90 Mon Sep 17 00:00:00 2001 From: Hanarion Date: Mon, 21 Apr 2025 00:09:47 +0200 Subject: [PATCH 14/15] Fix the prefix counts on VyOS, using the prefix count command and a new template --- napalm_vyos/vyos.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/napalm_vyos/vyos.py b/napalm_vyos/vyos.py index 268315e..80ed023 100644 --- a/napalm_vyos/vyos.py +++ b/napalm_vyos/vyos.py @@ -499,6 +499,26 @@ class VyOSDriver(NetworkDriver): return bgp_neighbor_data + def get_bgp_neighbor_prefix_counts(self, neighbor_address): + output = self.device.send_command(f"show bgp {self._get_ip_version(neighbor_address)} neighbor {neighbor_address} prefix-counts") + + current_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join( + current_dir, "templates", "bgp_prefix_counts.template" + ) + + with open(template_path) as template_file: + fsm = textfsm.TextFSM(template_file) + result = fsm.ParseText(output) + + if not result: + return {} + + neighbor_dict = dict(zip(fsm.header, result[0])) + + + return neighbor_dict + def get_bgp_neighbors_detail(self, neighbor_address=""): def safe_int(value, default=0): @@ -512,14 +532,16 @@ class VyOSDriver(NetworkDriver): neighbors = self.get_bgp_neighbors() for neighbor in neighbors["global"]["peers"]: - - output = self.device.send_command(f"show ip bgp neighbor {neighbor}") + neighbor_obj = neighbors["global"]["peers"].get(neighbor) + output = self.device.send_command(f"show bgp {self._get_ip_version(neighbor)} neighbor {neighbor}") current_dir = os.path.dirname(os.path.abspath(__file__)) template_path = os.path.join( current_dir, "templates", "bgp_details.template" ) + neighbor_prefix_counts = self.get_bgp_neighbor_prefix_counts(neighbor) + with open(template_path) as template_file: fsm = textfsm.TextFSM(template_file) result = fsm.ParseText(output) @@ -542,7 +564,7 @@ class VyOSDriver(NetworkDriver): "remote_as": int(neighbor_detail["REMOTE_AS"]), "router_id": neighbor_detail["LOCAL_ROUTER_ID"], "local_address": neighbor_detail[ - "LOCAL_ROUTER_ID" + "LOCAL_HOST" ], # Adjusted from LOCAL_ROUTER_ID based on context "routing_table": f"IPv{neighbor_detail['BGP_VERSION']} Unicast", # Constructed value "local_address_configured": bool(neighbor_detail["LOCAL_ROUTER_ID"]), @@ -601,16 +623,16 @@ class VyOSDriver(NetworkDriver): neighbor_detail["CONFIGURED_KEEPALIVE_INTERVAL"] ), "active_prefix_count": int( - neighbor_detail.get("ACTIVE_PREFIX_COUNT", 0) + neighbor_prefix_counts.get("BEST_SELECTED", 0) ), # Assuming ACTIVE_PREFIX_COUNT is available "accepted_prefix_count": int( - neighbor_detail.get("ACCEPTED_PREFIX_COUNT", 0) + neighbor_prefix_counts.get("USEABLE", 0) ), # Assuming ACCEPTED_PREFIX_COUNT is available "suppressed_prefix_count": int( - neighbor_detail.get("SUPPRESSED_PREFIX_COUNT", 0) + neighbor_prefix_counts.get("REMOVED", 0) ), # Assuming SUPPRESSED_PREFIX_COUNT is available "advertised_prefix_count": int( - neighbor_detail.get("ADVERTISED_PREFIX_COUNT", 0) + neighbor_obj.get("advertised_prefix_count", 0) ), "received_prefix_count": safe_int( neighbor_detail.get("RECEIVED_PREFIXES_IPV4", 0) From 3a238987acb7eb4bd33415877b71d5d4f3fa8618 Mon Sep 17 00:00:00 2001 From: Hanarion Date: Mon, 21 Apr 2025 00:10:53 +0200 Subject: [PATCH 15/15] Adding bgp_prefix_counts.template --- .../templates/bgp_prefix_counts.template | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 napalm_vyos/templates/bgp_prefix_counts.template diff --git a/napalm_vyos/templates/bgp_prefix_counts.template b/napalm_vyos/templates/bgp_prefix_counts.template new file mode 100644 index 0000000..e29538c --- /dev/null +++ b/napalm_vyos/templates/bgp_prefix_counts.template @@ -0,0 +1,26 @@ +Value NEIGHBOR_IP (\S+) +Value ADJ_IN (\d+) +Value DAMPED (\d+) +Value REMOVED (\d+) +Value HISTORY (\d+) +Value STALE (\d+) +Value VALID (\d+) +Value ALL_RIB (\d+) +Value PFXCT_COUNTED (\d+) +Value BEST_SELECTED (\d+) +Value USEABLE (\d+) +Value UNSORTED (\d+) + +Start + ^Prefix counts for ${NEIGHBOR_IP}, IPv[4,6] Unicast + ^\s*Adj-in:\s+${ADJ_IN} + ^\s*Damped:\s+${DAMPED} + ^\s*Removed:\s+${REMOVED} + ^\s*History:\s+${HISTORY} + ^\s*Stale:\s+${STALE} + ^\s*Valid:\s+${VALID} + ^\s*All RIB:\s+${ALL_RIB} + ^\s*PfxCt counted:\s+${PFXCT_COUNTED} + ^\s*PfxCt Best Selected:\s+${BEST_SELECTED} + ^\s*Useable:\s+${USEABLE} + ^\s*Unsorted:\s+${UNSORTED}