napalm-danos/napalm_vyos/vyos.py

798 lines
24 KiB
Python

# 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 os
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.disconnect()
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_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:
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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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