# Copyright 2016 Dravetech AB. All rights reserved. # # The contents of this file are licensed under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with the # License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. """ Napalm driver for VyOS. Read napalm.readthedocs.org for more information. """ import re import vyattaconfparser from netmiko import ConnectHandler from netmiko import SCPConn # NAPALM base from napalm_base.base import NetworkDriver from napalm_base.exceptions import ConnectionException, SessionLockedException, \ MergeConfigException, ReplaceConfigException,\ CommandErrorException class VyOSDriver(NetworkDriver): _MINUTE_SECONDS = 60 _HOUR_SECONDS = 60 * _MINUTE_SECONDS _DAY_SECONDS = 24 * _HOUR_SECONDS _WEEK_SECONDS = 7 * _DAY_SECONDS _YEAR_SECONDS = 365 * _DAY_SECONDS _DEST_FILENAME = "/var/tmp/candidate_running.conf" _BACKUP_FILENAME = "/var/tmp/backup_running.conf" _BOOT_FILENAME = "/config/config.boot" def __init__(self, hostname, username, password, timeout=60, optional_args=None): self._hostname = hostname self._username = username self._password = password self._timeout = timeout self._device = None self._scp_client = None self._new_config = None self._old_config = None self._ssh_usekeys = False if optional_args is None: optional_args = {} self._port = optional_args.get('port', 22) self._ssh_keyfile = optional_args.get('ssh_keyfile', None) if self._ssh_keyfile != None: self._ssh_usekeys = True def open(self): device = { 'device_type': 'vyos', 'ip': self._hostname, 'username': self._username, 'password': self._password, 'use_keys': self._ssh_usekeys, 'key_file': self._ssh_keyfile, 'port' : self._port, # optional, defaults to 22 'secret': 'secret', # optional, defaults to '' 'verbose': False, # optional, defaults to False } self._device = ConnectHandler(**device) self._scp_client = SCPConn(self._device) def close(self): self._device.close() def load_replace_candidate(self, filename=None, config=None): """ Only configuration files are supported with load_replace_candidate. It must be a full config file like /config/config.boot Due to the OS nature, we do not support a replace using a configuration string. """ if filename is not None: if os.path.exists(filename) == True: self._scp_client.scp_transfer_file(filename, self._DEST_FILENAME) print self._device.send_command("cp "+self._BOOT_FILENAME+" "+self._BACKUP_FILENAME) output_loadcmd = self._device.send_config_set(['load '+self._DEST_FILENAME]) match = re.findall("Load complete.", output_loadcmd) if not match: raise ReplaceConfigException("Failed replace config: " +output_loadcmd) else: raise ReplaceConfigException("config file is not found") else: raise ReplaceConfigException("no configuration found") def load_merge_candidate(self, filename=None, config=None): """ Only configuration in set-format is supported with load_merge_candidate. """ if filename is not None: if os.path.exists(filename) == True: with open(filename) as f: print 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 is not ""] self._device.send_config_set(cfg) else: raise MergeConfigException("config file is not found") elif config is not None: self._new_config = config else: raise MergeConfigException("no configuration found") def discard_config(self): self._device.send_config_set(['discard']) 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: return else: return output_compare def commit_config(self): self._device.send_config_set(['commit', 'save']) def rollback(self, filename=None): """Rollback configuration to filename or to self.rollback_cfg file.""" 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) else: self._device.send_config_set(['commit', 'save']) def get_environment(self): """ 'vmstat' output: procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- 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 = self._device.send_command("vmstat").split("\n")[-1] cpu = 100 - int(output_cpu.split()[-2]) """ 'free' output: total used free shared buffers cached Mem: 508156 446784 61372 0 139624 139360 -/+ buffers/cache: 167800 340356 Swap: 0 0 0 """ output_ram = self._device.send_command("free").split("\n")[1] available_ram, used_ram = output_ram.split()[1:3] environment = { "fans": { "status": None }, "temperature": { "temperature": None, "is_alert" : None, "is_critical": None }, "power": { "status" : None, "capacity": None, "output" : None }, "cpu": { "0": { "%usage": cpu }, }, "memory": { "available_ram": int(available_ram), "used_ram" : int(used_ram) } } return environment def get_interfaces(self): """ "show interfaces" output example: Interface IP Address S/L Description --------- ---------- --- ----------- br0 - u/D eth0 192.168.1.1/24 u/u Management eth1 192.168.1.2/24 u/u eth2 192.168.3.1/24 u/u foobar 192.168.2.2/24 lo 127.0.0.1/8 u/u ::1/128 """ output_iface = self._device.send_command("show interfaces") # Collect all interfaces' name and status match = re.findall("(\S+)\s+[:\-\d/\.]+\s+([uAD])/([uAD])", output_iface) # '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} output_conf = self._device.send_command("show configuration") # Convert the configuration to dictionary config = vyattaconfparser.parse_conf(output_conf) iface_dict = dict() for iface_type in config["interfaces"]: ifaces_detail = config["interfaces"][iface_type] for iface_name in ifaces_detail: description = self._get_value("description", ifaces_detail[iface_name]) speed = self._get_value("speed", ifaces_detail[iface_name]) hw_id = self._get_value("hw-id", ifaces_detail[iface_name]) is_up = (iface_state[iface_name]["Link"] == "u") is_enabled = (iface_state[iface_name]["State"] == "u") iface_dict.update({ iface_name: { "is_up" : is_up, "is_enabled" : is_enabled, "description" : description, "last_flapped" : -1, "speed" : speed, "mac_address" : hw_id } }) return iface_dict # for avoiding KeyError @staticmethod def _get_value(key, target_dict): if key in target_dict: return target_dict[key] else: return None def get_arp_table(self): # 'age' is not implemented yet """ 'show arp' output example: Address HWtype HWaddress Flags Mask Iface 10.129.2.254 ether 00:50:56:97:af:b1 C eth0 192.168.1.134 (incomplete) eth1 192.168.1.1 ether 00:50:56:ba:26:7f C eth1 10.129.2.97 ether 00:50:56:9f:64:09 C eth0 192.168.1.3 ether 00:50:56:86:7b:06 C eth1 """ output = self._device.send_command("show arp") output = output.split("\n") # Skip the header line output = output[1:-1] arp_table = list() 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=None else: macaddr=unicode(line[2]) arp_table.append({ "interface" : unicode(line[-1]), "mac" : macaddr, "ip" : unicode(line[0]), "age" : None }) return arp_table def get_ntp_stats(self): """ 'ntpq -np' output example remote refid st t when poll reach delay offset jitter ============================================================================== 116.91.118.97 133.243.238.244 2 u 51 64 377 5.436 987971. 1694.82 219.117.210.137 .GPS. 1 u 17 64 377 17.586 988068. 1652.00 133.130.120.204 133.243.238.164 2 u 46 64 377 7.717 987996. 1669.77 """ output = self._device.send_command("ntpq -np").split("\n")[2:] ntp_stats = list() for ntp_info in output: 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("(\d+\.\d+\.\d+\.\d+)", remote) ip = match.group(1) when = when if when != '-' else 0 ntp_stats.append({ "remote" : unicode(ip), "referenceid" : unicode(refid), "synchronized": synchronized, "stratum" : int(st), "type" : unicode(t), "when" : int(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").split("\n")[2:] ntp_peers = dict() for line in output: match = re.search("(\d+\.\d+\.\d+\.\d+)\s+", line) ntp_peers.update({ unicode(match.group(1)): {} }) return ntp_peers def get_bgp_neighbors(self): # 'description', 'sent_prefixes' and 'received_prefixes' are not implemented yet """ 'show ip bgp summary' output example: BGP router identifier 192.168.1.2, local AS number 64520 IPv4 Unicast - max multipaths: ebgp 1 ibgp 1 RIB entries 3, using 288 bytes of memory Peers 3, using 13 KiB of memory Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd 192.168.1.1 4 64519 7226 7189 0 0 0 4d23h40m 1 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").split("\n") match = re.search(".* router identifier (\d+\.\d+\.\d+\.\d+), local AS number (\d+)", output[0]) if not match: return {} router_id = unicode(match.group(1)) local_as = int(match.group(2)) bgp_neighbor_data = dict() bgp_neighbor_data["global"] = dict() 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 is not ""] for i in bgp_info: peer_id , bgp_version, remote_as, msg_rcvd, msg_sent, table_version, \ in_queue, out_queue, up_time, state_prefix = i.split() 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: 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("remote router ID (\d+\.\d+\.\d+\.\d+).*", bgp_detail) remote_rid = match_rid.group(1) match_prefix_accepted = re.search("(\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" : is_enabled, "local_as" : local_as, "is_up" : is_up, "remote_id" : unicode(remote_rid), "uptime" : self._bgp_time_conversion(up_time), "remote_as" : int(remote_as) } af_dict = dict() af_dict[address_family] = { "sent_prefixes" : None, "accepted_prefixes": int(accepted_prefixes), "received_prefixes": received_prefixes } peer_dict["address_family"] = af_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"]) if "never" in bgp_uptime: return -1 else: if "y" in bgp_uptime: match = re.search("(\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("(\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("(\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 def get_interfaces_counters(self): # 'rx_unicast_packet', 'rx_broadcast_packets', 'tx_unicast_packets', # 'tx_multicast_packets' and 'tx_broadcast_packets' are not implemented yet """ 'show interfaces detail' output example: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 00:50:56:86:8c:26 brd ff:ff:ff:ff:ff:ff ~~~ RX: bytes packets errors dropped overrun mcast 35960043 464584 0 221 0 407 TX: bytes packets errors dropped carrier collisions 32776498 279273 0 0 0 0 """ output = self._device.send_command("show interfaces detail") interfaces = re.findall("(\S+): <.*", output) count = re.findall("(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+", output) counters = dict() j = 0 for i in count: if j % 2 == 0: rx_errors = i[2] rx_discards = i[3] rx_octets = i[0] rx_unicast_packets = i[1] rx_multicast_packets = i[5] rx_broadcast_packets = None else: counters.update({ interfaces[j / 2]: { "tx_errors" : i[2], "tx_discards" : i[3], "tx_octets" : i[0], "tx_unicast_packets" : i[1], "tx_multicast_packets": None, "tx_broadcast_packets": None, "rx_errors" : rx_errors, "rx_discards" : rx_discards, "rx_octets" : rx_octets, "rx_unicast_packets" : rx_unicast_packets, "rx_multicast_packets": rx_multicast_packets, "rx_broadcast_packets": rx_broadcast_packets } }) j += 1 return counters def get_snmp_information(self): # 'acl' is not implemented yet output = self._device.send_command("show configuration") # convert the configuration to dictionary config = vyattaconfparser.parse_conf(output) snmp = dict() snmp["community"] = dict() try: for i in config["service"]["snmp"]["community"]: snmp["community"].update({ i: { "acl": None, "mode": config["service"]["snmp"]["community"][i]["authorization"] } }) snmp.update({ "contact": config["service"]["snmp"]["contact"], "location": config["service"]["snmp"]["location"] }) return snmp except KeyError: return {} def get_facts(self): output_uptime = self._device.send_command("cat /proc/uptime | awk '{print $1}'") uptime = int(float(output_uptime)) output = self._device.send_command("show version").split("\n") ver_str = [line for line in output if "Version" in line][0] version = self.parse_version(ver_str) sn_str = [line for line in output if "S/N" in line][0] snumber = self.parse_snumber(sn_str) hwmodel_str = [line for line in output if "HW model" in line][0] hwmodel = self.parse_hwmodel(hwmodel_str) output = self._device.send_command("show configuration") config = vyattaconfparser.parse_conf(output) if "host-name" in config["system"]: hostname = config["system"]["host-name"] else: hostname = None if "domain-name" in config["system"]: fqdn = config["system"]["domain-name"] else: fqdn = None iface_list = 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" : unicode(version), "serial_number" : unicode(snumber), "model" : unicode(hwmodel), "hostname" : unicode(hostname), "fqdn" : unicode(fqdn), "interface_list": iface_list } return facts @staticmethod def parse_version(ver_str): version = ver_str.split()[-1] return version @staticmethod def parse_snumber(sn_str): sn = sn_str.split(":") return sn[1].strip() @staticmethod def parse_hwmodel(model_str): model = model_str.split(":") return model[1].strip() def get_interfaces_ip(self): output = self._device.send_command("show interfaces") output = output.split("\n") # delete the header line and the interfaces which has no ip address ifaces = [x for x in output[3:-1] if "-" not in x] ifaces_ip = dict() for iface in ifaces: iface = iface.split() if len(iface) != 1: iface_name = iface[0] # Delete the "Interface" column iface = iface[1:-1] # Key initialization ifaces_ip[iface_name] = dict() 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][ip_addr] = { "prefix_length": mask } return ifaces_ip @staticmethod def _get_ip_version(ip_address): if ":" in ip_address: return "ipv6" elif "." in ip_address: return "ipv4" def get_users(self): output = self._device.send_command("show configuration commands").split("\n") 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_auth = dict() for user in user_name: sshkeys = list() # extract the configuration which relates to 'user' for line in [x for x in user_conf if user in x]: # "set system login user alice authentication encrypted-password 'abc'" 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'" elif len(line) == 10 and line[8] == "key": sshkeys.append(line[9].strip("'")) user_auth.update({ user: { "level": level, "password": password, "sshkeys": sshkeys } }) return user_auth def ping(self, destination, source="", ttl=255, timeout=5, size=100, count=5): # does not support multiple destination yet command = "ping %s " % destination command += "ttl %d " % ttl command += "deadline %d " % timeout command += "size %d " % size command += "count %d " % count if source != "": command += "interface %s " % source ping_result = dict() output_ping = self._device.send_command(command) if "Unknown host" in output_ping: err ="Unknown host" else: err ="" if err is not "": ping_result["error"] = err else: # 'packet_info' example: # ['5', 'packets', 'transmitted,' '5', 'received,' '0%', 'packet', 'loss,', 'time', '3997ms'] packet_info = output_ping.split("\n")[-2] packet_info = [x.strip() for x in packet_info.split()] sent = int(packet_info[0]) received = int(packet_info[3]) lost = sent - received # 'rtt_info' example: # ["0.307/0.396/0.480/0.061"] rtt_info = output_ping.split("\n")[-1] match = re.search("([\d\.]+)/([\d\.]+)/([\d\.]+)/[\d\.]+", rtt_info) if match is not None: rtt_min = float(match.group(1)) rtt_avg = float(match.group(2)) else: rtt_min = None rtt_avg = None ping_result["success"] = dict() ping_result["success"] = { "probes_sent": sent, "packet_loss": lost, "rtt_min" : rtt_min, "rtt_avg" : rtt_avg, "rtt_stdev" : None, "results" : {"ip_address": destination, "rtt": rtt_avg} } return ping_result